tritium

Regression gates

The frozen reference for the hardware-agnostic pivot. Every number here was measured on the pre-pivot tree at commit c3351d1 (v0 .trit, scalar kernel, single-threaded) before any pivot code landed.

Rule: nothing in the pivot may change a number in this file without an explicit, argued entry in the "Sanctioned changes" section below.

Environment of record

Host AMD Ryzen 9 8945HX (Zen 4), 32 threads
ISA avx2 avx512f avx512bw avx512dq avx512vl avx512_vnni avx512_vpopcntdq avx512_bf16 gfni
rustc 1.95.0 (59807616e 2026-04-14)
Verilator 5.020
Yosys 0.33
Model models/bitnet-2b4t.trit, 1,836,137,811 bytes, .trit v0

Other hosts will differ on timing and RSS. The correctness gates are host-independent and must hold everywhere.

G1 — Unit and integration tests

cargo test --workspace

All pass. trit-core 12, tritsim lib 19, tiny_model.rs 6.

G2 — Real-checkpoint logit agreement (the headline gate)

tritsim compare --model models/bitnet-2b4t.trit --dump logits.json
6 positions: mean cosine 0.9991, top1 match 100.0%

Last position, top-3 both sides:

ours   [(12366, 18.34601), (539, 14.126393), (264, 13.815544)]
theirs [(12366, 18.5),     (539, 14.25),     (264, 13.75)]

Single-position variant (--dump bos_dump.json): 1 positions: mean cosine 0.9977, top1 match 100.0% — identical under TRITSIM_INT_MLP=1.

CLI acceptance thresholds (crates/tritsim/src/main.rs): cosine >= 0.98, top-1 >= 0.90. The measured values sit far above them; the gate is the measured value, not the threshold.

G3 — Greedy text, byte-exact

tritsim run --model models/bitnet-2b4t.trit \
            --tokenizer models/bitnet-2b4t/tokenizer.json \
            --prompt "The capital of France is" --steps 16
 Paris. Paris is a city in the north of France, and it is the

Must reproduce byte-for-byte in Reference, Folded and IntMlp modes, and on every backend (cpu, rtl) and every CPU kernel.

G4 — RTL bit-exactness

make -C rtl lint          # clean
make -C rtl test          # all sets, exact i32 equality
SET vectors/exact_64x64:      64 rows OK
SET vectors/padded_5x100:      5 rows OK
SET vectors/extremes_4x128:    4 rows OK
SET vectors/zeros_3x64:        3 rows OK
SET vectors/wide_2x6912:       2 rows OK
SET vectors/model_k_proj_l0:   8 rows OK
SET <invalid-code>:            err raised OK

extremes_4x128 feeds x = -128 deliberately. Any CPU kernel that negates in the i8 domain fails here. This set must never be weakened.

G5 — Multiplier-free synthesis

make -C rtl synth

Yosys asserts zero $mul and zero $macc cells. Baseline cell count 33,659 (MAX_COLS chparam'd to 512; $_DFFE_PP_ 4096 is the flattened activation memory). Generic synthesis, no device target.

G6 — Hardware-in-the-loop parity

cargo test -p tritsim --features rtl --release

random_shapes_match_cpu_exactly compares the Verilated core against the CPU kernel on 8 shapes (1x64, 3x64, 2x100, 5x129, 7x640, 2x2560, 2x6912, 16x61) and requires exact equality. All pass.

G7 — Resource baseline (the numbers the pivot exists to move)

Metric Pre-pivot Why
Peak RSS, real model 6,401,024 KB (6.10 GiB) read_trit unpacks to Vec<i8> (~2.08 GB), embeddings f32 (~1.31 GB), lm_head cloned from embed when tied (another ~1.31 GB)
Model load + 1 forward ~33 s dominated by unpack + copy
Decode ~6.3 s/token scalar, branch-per-weight, single-threaded
Bytes touched per token 521 MB ternary + 1,313 MB lm_head f32 lm_head is 2.5x the ternary weights

These are targets to improve, not invariants. G1-G6 are invariants.

G8 — trit-core against the tritsim oracle

Two independent implementations of the same model must agree.

cargo test -p tritsim --release --test cross_implementation              # tiny fixtures
cargo test -p tritsim --release --test cross_implementation -- --ignored # real checkpoint

On the real checkpoint, 8 positions, all three numerics rungs: cosine 1.000000 and identical top-1 at every position.

This gate found a real bug. The production path had precomputed w_scale * x_scale into a single constant and multiplied once, where the reference multiplies twice: acc as f32 * w_scale * x_scale. f32 multiplication is not associative, so acc * (w * x) and (acc * w) * x round differently. The drift was invisible at short contexts (cosine 1.000000 for four positions), grew with position to 0.999624 by position 7, and changed the 16th generated token from "the" to "known". Ordering the multiplications to match the reference restored cosine 1.000000 everywhere.

Post-pivot results

The pivot's purpose was to move the G7 numbers without moving G1-G6. Measured on the environment of record with models/bitnet-2b4t.trit:

Metric Pre-pivot After the pivot After A2+A3
Decode 0.16 tok/s 14.89 tok/s 27.90 tok/s
Time to first token ~33 s 470 ms 239 ms
Peak RSS 6,401,024 KB 1,891,184 KB 1,249,280 KB
Bytes per token 1,834,352,640 1,834,352,640 1,177,681,920
Achieved bandwidth ~0.3 GB/s 27.3 GB/s 32.9 GB/s

Decode is at 4 threads, which is where this host peaks.

The roofline fraction is a ratio against a probe run in the same invocation, and that probe is itself noisy: across the five thread counts in the committed CSV it read 48.25, 58.27, 57.24, 52.67 and 49.78 GB/s. At 4 threads, 32.85 against 57.24 is 57%. Quote the pair, never the fraction alone, and never a fixed ceiling for this host. The pivot's "66% of roofline" was computed against a single-pass probe that under-reported, so it was optimistic on top of that.

Kernel throughput on a 2560x6912 tensor, planes resident in L3:

Kernel ms GB/s vs scalar
scalar 9.10 0.5 1.00x
avx512vnni 0.28 15.8 32.45x
avx512bw 0.38 11.6 23.91x
avx2 0.66 6.7 13.87x
bitserial (popcount) 6.12 0.7 1.49x

Against other runtimes

Same host, same session, greedy, 32 tokens, median of three. bitnet.cpp runs the identical checkpoint; llama.cpp runs Qwen2.5-3B Q4_K_M because mainline cannot load i2_s, so that column is a different model at a different quality point.

threads tritium llama.cpp bitnet.cpp
1 19.40 15.40 13.61
2 26.73 22.06 19.10
4 27.90 27.83 26.72
8 25.93 26.18 32.49
16 20.72 23.88 31.21

Ahead at 1, 2 and 4 threads, 1.43x bitnet.cpp per core. Still behind above 4 threads: our curve peaks and declines where bitnet.cpp keeps climbing to 32.49. That gap is the remaining scaling work.

Threading

Measured, not assumed, and the measurement changed twice.

Before a persistent pool existed, splitting each of the 210 ternary matvecs per token across the machine cost more than it saved: 14.13 tok/s at one thread against 2.40 at 32. PARALLEL_MIN_BYTES was set to 32 MiB to prevent it.

With the pool, dispatch is a sequence-counter bump and an unpark, and the threshold drops to 64 KiB. Four mechanisms were measured in the real decode loop; a dedicated work-stealing pool and a broadcast primitive both stayed slower than not parallelising at all.

Mechanism 1t 2t 4t 8t
serial 37.6 37.6 36.9 36.7
rayon, global pool 36.0 65.0 108.8 174.0
rayon, sized pool 35.8 50.0 69.7 148.9
rayon, broadcast 35.9 47.2 57.5 89.8
persistent spin barrier 37.1 20.0 12.8 14.1

Layer time in ms. All produce byte-identical output.

G1-G6 are unchanged throughout. G2 still reports 0.9991/100%, G3 is still byte-identical in every numerics mode on both implementations, and the real checkpoint still decodes byte-identically through the Verilated RTL core -- 24.4 s/token in simulation, via tritd --backend rtl.

Sanctioned changes

Numbers here move only with an entry below, naming the phase, the reason, and the before/after.

Phase What changed Before After Reason
B .trit v0 -> v1 bit planes v0 codes v1 planes The migration is proven exact: tritc upgrade of the v0 file is byte-identical to a fresh convert. G2 and G3 unchanged.
B Recorded zero fraction 0.377 0.4219 The old figure was hand-transcribed into checkpoint-notes.md and wrong. The byte-identity of the upgrade proves the trits themselves did not change.
C RTL weight interface w_data[127:0] w_pos/w_neg Synthesis is unchanged at 33,659 cells, still multiplier-free.
A2 Automatic thread count one per core one per core, max 8 Interleaved pairs give 1.17x at 2, 1.18x at 4, 1.17x at 8 and 0.94x at 16. One per core is the wrong automatic answer on a large machine. Explicit --threads is unchanged.
A3 Dense 2-D tensor storage f32 bf16 when the source is bf16 The checkpoint is bf16 and tritc was widening it, so this restores the source precision rather than reducing it. Every value round-trips. Bytes per token 1,834,352,640 -> 1,177,681,920; G2 and G3 unchanged. Existing .trit files still load; the benefit needs a re-convert, and narrowing an existing file is not offered because a stored f32 does not record whether it was bf16 first.
A3 Bandwidth probe single pass best of five The single-pass probe scattered 45-50 GB/s where the machine sustains ~53, so roofline_pct flattered every result. Reported fraction drops from ~62% to 53-57% with no runtime change.

The one change already anticipated: RoPE currently computes theta.powf(-2.0 * i / head_dim) in f32 (crates/tritsim/src/math.rs), giving angle errors up to ~1e-3 rad at position 2048. Moving both implementations onto a shared f64 inv_freq/rope_angle is expected to shift G2 slightly — in the direction of the reference, since the reference is more accurate. That gets its own isolated phase and its own row above.