GitHub - ValerioDolci/bitbop: BitBop: latent-free ternary training of small language models

GitHub

8 min read Original article ↗

License: MIT Model: BitBop-125M Model: BitBop-91M Technical report BitBop II report

BitBop trains ternary language models — every weight is in {-1, 0, +1} — without keeping full-precision latent weights in the ternary body. Instead of a shadow float copy per weight (the STE recipe), the only per-weight training state is a small bf16 flip momentum (the tied token embedding and output head stay float with Adam — a small fraction of the parameters). That makes the optimizer state ~5× smaller, so larger models fit in the same GPU memory, and the shipped weights are int2-packed (2 bits/weight stored; ~1.3 bits of entropy at ~63% sparsity, below BitNet's 1.58).

This repository is the reference implementation and the reproduction kit for the technical report. It is a proof of concept, trained on a single RTX 3060 (6 GB) and an Apple M4 — not a production model.

Peak training memory and wall-clock: a 325M-class model completes a full step in 6 GB only with BitBop; STE and float do not.

A 325M-class model trains within a single 6 GB GPU only with BitBop; the STE and float baselines spill or do not complete (see §R2 of the report).

What we claim (and what we don't)

Claim Scope
Quality BitBop beats STE — the fair, same-family paired comparison (CI-backed, up to 91M) — and is competitive with an untuned same-recipe float at a matched training budget. On BabyLM a 125M ternary model reaches parity with a same-data GPT-2‑124M (float): 68.0 vs 67.9 BLiMP, same harness — but single-seed, and the 0.16-pt margin is inside the measured seed spread (0.11), so a tie, not a win. budget-matched, ≤125M; not asymptotic
Memory ~2.3 bytes/param of optimizer state (ternary body) vs 6 (Adam8bit) / 12 (AdamW). A 325M-class model trains in 6 GB — only BitBop completes a full step there (the 5× and the 6 GB fit are measured on the full model, float embed/head included). training-state memory
Inference int2-packed weights, 2 bpw stored (~1.3 bpw entropy at ~63% sparsity), ~16× smaller than fp32. Peak RAM/speed unchanged without a dedicated int2 kernel. storage only
Speed Not a claim. There is no ad-hoc kernel yet, so no wall-clock speedup. Ternary weights do remove the multiplies (adds/subtracts only) — the potential is there, the kernel is future work. —

We are not claiming state of the art, a speedup, or anything above 325M-class.

On the baselines. All three variants (bop / ste / float) share a single recipe — thresholds, schedule and batch were tuned for BitBop; lr 1e-3, no warmup, no gradient clipping — and the float and STE baselines are not separately tuned. The comparison isolates the training algorithm at fixed hyperparameters; it is not evidence that BitBop beats a well-tuned float, which would likely be stronger. The robust, statistically-backed result is BitBop > STE (same ternary family, same recipe).

Results (all same-harness, our validated BLiMP harness)

BLiMP macro-accuracy (67 phenomena × 1000 minimal pairs):

Model Params Training data BLiMP
BitBop (ternary) 125M BabyLM strict-small (~10M words) 68.0
BitBop (ternary) 50M BabyLM strict-small 66.8
BitBop (ternary) 25M BabyLM strict-small 66.0
STE ternary (baseline) 25M BabyLM strict-small 63.2
GPT-2‑124M (float) 124M BabyLM 10M (same data) 67.9
BabyLlama‑58M (distilled) 58M BabyLM 10M 68.9
GPT-2‑124M (float, data-rich) 124M WebText (≫ data) 82.5

The data-rich GPT-2 (82.5) is the data ceiling we do not reach. The point is that a ternary model at the same data budget reaches parity within noise with a float GPT-2 of the same size (see the tie caveat above).

On TinyStories (internal, budget-matched), BitBop-91M reaches 1.49 validation nats vs STE 1.58 and float 1.59 — sample efficiency at a matched, under-trained budget (~1 token/param, far below compute-optimal), not an asymptotic result. The STE gap is the fair one (same recipe, same family); the float number uses BitBop's untuned recipe and should not be read as beating float. See the technical report.

What's new — BitBop II (July 2026)

The follow-up report (BitBop II, PDF) closes the three questions the first report left open — more than three weight states, a controlled canary memorization study, and parameters-vs-bits at matched memory.

BitBop II: the quality-bits frontier and the controller that breaks it — concave n-level frontier (toy scale), the horizon-free controller lifts the big ternary off the frontier down to the float anchor; inset: at 25M, 5.3x less training state than AdamW. Everything below is measured at ≤25M params, two small corpora (TinyStories / BabyLM strict-small), seeds {0,1}, with pre-registered designs and the falsified predictions reported as such.

Finding One line Scope
N-level frontier Generalising the flip rule to 5/9/15 weight levels traces a concave quality-bits frontier that reproduces across three scales — but it was measured under a fixed momentum rate, a regime that specifically handicaps ternary via late-training weight churn. toy → 25M
Horizon-free control (T4) A validation-plateau ratchet on the momentum rate replaces the hand-tuned schedule; it rescues ternary (−0.185 nats at 25M, causal, 2 seeds) and barely moves 9-level. Its value is timing: early flips preserved, late churn suppressed 18×. A constant-high momentum control fails (early flips starved 12.8×). 25M
Levels lose their case At matched storage, under the controller, the big ternary model beats the 15-level one by 0.12 nats (2 seeds) — reversing our own earlier fixed-momentum finding. What levels were buying was churn robustness, which the controller provides for free. toy, matched bytes (≈3× FLOPs caveat)
The float baseline overfits small data; ternary does not At 17 epochs on BabyLM-small the same-recipe float overfits (min 2.94 → 3.00); every ternary variant is monotone and ends below the float's best checkpoint. A true-STE control attributes this to reduced fit capacity, not the optimiser. Regime-bound: with more data this could reverse. 25M, 2 seeds
Latent-free ≥ STE, again The flip optimiser matches STE+AdamW (nominally 0.02–0.03 nats better, within the measured dtype bound) at ≈5× less per-weight training state. BLiMP vs float: parity (~.66 under one harness). 25M, 2 seeds
Controlled canary memorization With Carlini-style canaries, ternary flip training exposes far less than float (5.3–6.7 vs 13.0–13.3 bits at 64×); the low exposure survives the controller (≈0% of the gap at 16×, 21–31% at 64×), and a designed worst-case falsifies "high momentum crystallises": exposure tracks fit, not the momentum rate. toy, 2 seeds

One practitioner's warning worth repeating: STE breaks silently under autocast — the straight-through pattern w+(q-w).detach() stops training the quantised path in bf16 autocast and degenerates to float training. Our first STE control was invalid for exactly this reason; the diagnosis and the fp32 rule are in the report. The latent-free optimiser is immune (no latent to bypass).

Quickstart (~15 min on CPU, a few minutes on GPU)

Reproduce the headline BabyLM comparison from published weights:

pip install -r requirements.txt
bash scripts/prepare_data.sh          # downloads BLiMP
bash scripts/reproduce_babylm.sh      # downloads BitBop-125M + baselines, runs BLiMP

This downloads Feyd89/BitBop-125M-babylm from the Hugging Face Hub, evaluates it and the public baselines (GPT-2, BabyLlama) with the same harness, and prints the table above.

How it works

STE keeps a full-precision latent weight plus two Adam moments (~12 B/param); BitBop keeps only the packed ternary weight and a bf16 flip momentum (~2.3 B/param) and updates the weight directly.

In five lines:

  • Weights are ternary, stored 2-bit packed (4 trits/byte), unpacked just-in-time for the matmul.
  • The only per-weight state is m, an EMA of the negative gradient (bf16).
  • A trit flips when m crosses a threshold set from the per-row RMS of m — easy to fall into 0 (tau_in), harder to leave it (tau_out), which keeps the weights sparse.
  • beta (the EMA rate) follows a one-decade decay schedule; m resets on flipped entries.
  • No latent float weights are ever stored. Details and the full recipe in the technical report.

Train it yourself

# full reproduction of the 25M BabyLM point from scratch (~3h on a 6 GB GPU)
bash scripts/train_babylm_25m.sh
# or drive the trainer directly
python train.py --config configs/91m_bop.json --device cuda

Configs for every reported run are in configs/ (bop / ste / float, 91M TinyStories and the 25M/50M/125M BabyLM ladder).

Weights

On the Hugging Face Hub (MIT):

STE and float baselines are reproducible from configs/ (weights not shipped).

Layout

bitbop/kernel.py    int2 packing + BopTernaryLinear/Int2Lin + ScheduledBop (the flip optimizer)
bitbop/model.py     LMComposite (SWA body + full-attention layers, NoPE, tied interface)
bitbop/data.py      BPE tokenizer + packed .bin loader
train.py           one driver for bop | ste | float
eval_blimp.py      BLiMP harness (+ HF branch for external baselines)
configs/           one JSON per reported run
scripts/           data prep + reproduction
docs/BitBop-technical-report.pdf   reference technical report (PDF)

Citing / data

BitBop is MIT-licensed. It builds on public datasets and benchmarks — please cite their authors: TinyStories, the BabyLM Challenge (2024), and BLiMP. BabyLM data is not redistributed here; scripts/prepare_data.sh points you to the source.

Limitations

Budget-scoped quality claims only; ≤325M-class; single corpus per experiment; one author with an AI-assisted workflow. No speed claim without a dedicated kernel. See the reports for the full "tried and didn't work" table and the ongoing directions; BitBop II adds its own limitations section (data-limited 17-epoch regime, per-budget crossovers, matched bytes ≠ matched FLOPs).