Roadmap
How this document changed
The original roadmap was six numbered weeks ending in an FPGA demo box, written when the plan was "prove the primitive in simulation, buy a board, ship the demo." That plan produced everything it promised through its own week 3b, and then the project pivoted: the runtime became hardware-agnostic, and the same ternary primitive now runs usefully on commodity CPUs as well as on the fabric it was designed for.
Two things follow. First, week numbering is gone. It was already fiction —
the pivot alone took longer than a week — and this project has always actually
run on gates: a milestone is done when a stated, measured condition holds,
not when a calendar says so. Second, there are now two tracks. They share the
.trit format, the correctness gates and most of the code, and they are worked
in whatever order the evidence favours rather than strictly in sequence.
What has not changed: every milestone ends in a public artifact, every performance claim ships with a script that reproduces it, and misses get published rather than buried.
Done
| Outcome | Gate | |
|---|---|---|
| Ternary inference, correct | tritsim decodes BitNet b1.58 2B4T in pure Rust, no multiplies in the hot loop |
G2: mean logit cosine 0.9991 vs the HF reference, top-1 100% |
.trit v0 and the RTL core |
Multiplier-free SystemVerilog tritcore, driven by oracle-generated vectors |
G4: bit-exact on every set including the x = -128 extremes; G5: Yosys asserts zero $mul/$macc |
| Numerics | Per-stage ranges measured on the real checkpoint; norm folding removes the rsqrt and divide from the datapath | Identical compare metrics and greedy output, property-tested across 1000 adversarial vectors |
| Hardware in the loop | The real 2B model decodes end-to-end through the Verilated core | G6: byte-identical to the CPU backend, at 24.4 s/token |
| Integer-exact MLP | The squared-ReLU stage never exists in f32 | Greedy text character-identical on both prompts |
| The pivot | .trit v1 bit planes, SIMD kernels, host daemon, C ABI, comparative harness |
G7 moved 93x on decode; G1-G6 unmoved |
| Review hardening | 14 findings fixed, including an i16 overflow in the NEON kernel and a streaming detokenizer that destroyed every multi-byte codepoint |
NEON kernels executed on aarch64 hardware for the first time; all gates re-verified |
Where that leaves the numbers, measured on a Ryzen 9 8945HX over the four-prompt short suite. The frozen single-prompt reference in 06-GATES.md reads slightly lower (14.89 tok/s, 470 ms, 27.3 GB/s) because it is a different input, and it — not this table — is what a regression is measured against:
| Pre-pivot | Now | |
|---|---|---|
| Decode | 0.16 tok/s | 15.7 tok/s |
| Time to first token | ~33 s | 440 ms |
| Peak RSS | 6.10 GiB | 1.80 GiB |
| Achieved bandwidth | ~0.3 GB/s | 29.0 GB/s (~61-68% of the measured roofline) |
The board-purchase gate
Still open, and deliberately so. The selection memo (04b-BOARD-MEMO.md) recommends a Kria KV260 on roofline-versus-synthesis grounds, and the gate that was meant to unblock the purchase passed when the real model decoded through the Verilated core.
The pivot changed the urgency, not the conclusion. A working CPU runtime means the project has something demonstrable without a board, which removes the schedule pressure that would otherwise have forced the purchase early. The decision is Bahadir's and nothing here commits money.
Track A — the runtime
Ordered by evidence, not by preference. Each entry states why it is where it is, and the order changed once the baselines were actually run — measuring beat reasoning, which is the whole point of putting A1 first.
A1. Baselines, actually run — DONE
Both baselines are installed, measured and committed
(benches/results/DESKTOP-HV2MQTM-20260823.csv). Same host, same prompts,
greedy, 32 tokens, median of three invocations:
| threads | tritium | llama.cpp Q4_K_M | bitnet.cpp I2_S |
|---|---|---|---|
| 1 | 13.28 | 13.91 | 12.54 |
| 2 | 14.61 | 20.01 | 19.64 |
| 4 | 15.23 | 25.54 | 27.29 |
| 8 | 15.23 | 24.72 | 31.90 |
| 16 | 14.98 | 22.84 | 30.51 |
bitnet.cpp runs the identical checkpoint, so that is the apples-to-apples
column. llama.cpp runs Qwen2.5-3B Q4_K_M — mainline cannot load the i2_s
type and its converter has no BitNet entry — so it is a different model at a
different quality point, and the bits/w column carries that.
The answer was unflattering, which is what this item was for. All three runtimes land within 11% of each other at one thread. Only Tritium fails to scale: 1.15x from one thread to eight, against bitnet.cpp's 2.54x and llama.cpp's 1.78x, ending 2.09x behind bitnet.cpp.
Nothing here says the kernels are bad; per core we are unremarkable rather than ahead. Everything here says the runtime cannot use a machine.
An earlier version of this table claimed Tritium was the fastest of the three
per core, by 1.29x. That was a measurement artifact: our model file had been hot
in page cache for an hour of testing while the baseline GGUFs were freshly
downloaded and cold. benches/run.sh now pre-warms every model before
measuring, and tritd bench reports a median rather than its best run, so its
figure is produced the same way the baselines' are.
Two levers fall out of it, and they reorder the rest of this track.
A2. Parallel scaling
The largest lever in the project, by a wide margin. Tritium is ordinary per core and last by a factor of two in aggregate. If it scaled like bitnet.cpp, 13.28 tok/s at one thread becomes roughly 34 at eight — before any other change, and past bitnet.cpp's 31.90.
The cause is measured, not guessed. A decode step issues 210 ternary matvecs
whose largest is 4.4 MB, and each one currently pays a rayon fork/join.
Lowering PARALLEL_MIN_BYTES to let them parallelize makes things dramatically
worse, not better:
| threads | layer time |
|---|---|
| 1 | 37.4 ms |
| 2 | 66-74 ms |
| 4 | 112-115 ms |
| 8 | 173-175 ms |
Even two threads is 1.8x worse. So PARALLEL_MIN_BYTES is correct and must
stay until something cheaper than per-call fork/join exists. ggml gets its
scaling from a persistent pool with cheap barriers; that is the shape of the
fix, and it is a real piece of engineering rather than a tuning change.
Gate: decode tok/s at 1, 2, 4, 8 and 16 threads published next to the baseline curve, with the same suite. The number that matters is the slope, not the peak.
A3. The f32 LM head
Still worth doing, at a smaller number than first estimated, and now
independently corroborated: bitnet.cpp stores token_embd as f16 where Tritium
stores f32, so it moves ~1178 MB per token against our 1834 MB. Part of its
advantage is simply that.
The instrumented decode puts the head at 26.9 ms of a 64.4 ms token — 42% of the time, not the 72% the byte share suggests, because the head runs at 48.8 GB/s while the ternary path runs at 13.9. Halving its bytes should take the token to roughly 51 ms: about 1.26x, plus peak RSS from 1846 MB to around 1190 MB.
The values are already bf16 — tritc widens them from the checkpoint on the
way in and nothing ever adds precision — so storing and reading them as bf16 is
lossless relative to the source. There is no quality experiment to run, which
is why this ranks above anything that trades accuracy.
Gate: bytes/token and decode tok/s measured before and after, with G2 held at its frozen value.
A4. Energy per token
J/token is named "the headline metric" in 04-BENCHMARKS.md and has never been reported, because the reader refuses to estimate and no machine in the loop has exposed a counter. The development host has no RAPL zones at all.
Blocked on hardware access rather than on work. Until then the column stays
empty and the source reads none, which is the correct behaviour and not a
placeholder to be filled with a guess. Now that both baselines run on this
host, an energy-capable box would produce the comparison the project exists to
make.
Gate: J/token from a real counter, next to a baseline measured the same way on the same machine.
A5. ARM throughput
The NEON kernels are correct — executed on aarch64 CI hardware, matching the
reference across the differential corpus including the worst case for the i16
accumulators. They have never been timed, so no ARM performance claim exists.
This matters disproportionately for positioning: the edge devices this project targets are overwhelmingly ARM, so "verified on x86" is a weaker story than it looks. It sits below A2 only because the scaling defect would travel to ARM unchanged, and measuring it twice would be wasted work.
Gate: decode tok/s on real ARM hardware, published next to the x86 figures, with the kernel named.
A6. KV cache precision and context
f32 and preallocated: 315 MB at a 2048 cap. int8 pages would cut that substantially, and the ratio worsens as context grows. Below A2-A3 because it moves resident footprint rather than per-token traffic — RSS, not tok/s.
Gate: RSS measured before and after at equal context, G2 held.
A7. Batched prefill
Batch 1 everywhere today. Prefill is the compute-bound phase, so it is where
batching pays, and it is also what would make PARALLEL_MIN_BYTES worth
revisiting — after A2, and with a measurement.
Explicitly last: a throughput feature for a serving story the project does not yet have.
Track B — silicon
The RTL is not a museum piece and the pivot did not retire it. It is a backend that CI exercises on every push, its bit-exactness is a frozen invariant, and it is the only path to the efficiency numbers that justify the whole thesis. What changed is that it is no longer the only way to have a demo, which means it can be done properly rather than urgently.
B1. Board bring-up
Follows the purchase. Port the sequencer, get a single transformer layer running end-to-end on the part, output matching the oracle.
Gate: one layer on hardware, bit-identical to tritsim. Artifact: scope or
ILA capture next to the golden vectors.
B2. Achieved bandwidth against the roofline
The number the architecture document has been predicting without evidence. Measure achieved GB/s, lane utilization and clock, and compare to the roofline the board was chosen on.
Gate: measured GB/s published against the predicted figure, including the gap and its explanation.
B3. Full model on the part
Layer sequencer, host-driven descriptor chain, double-buffered DMA, KV cache with a BRAM hot window. Embeddings and head on the PS side if fabric-constrained — which, given section 3 of the architecture, is where they belong anyway.
Gate: a full decode on hardware, output matching the oracle, with tok/s and J/token measured on the 12V rail.
B4. Timing closure and the honest RTL limits
Two things are known-unfinished and should not be discovered by a board:
- The 64-term single-cycle reduction and 64 parallel activation reads are fine under Verilator and are not placed on a part.
MAX_COLSis 8192. Every projection in this model class fits, and the wrappers now reject anything wider rather than wrapping the address silently — but a larger model would need real banking.
Risks
| Risk | Mitigation |
|---|---|
| A baseline beats us on the same host | Publish it. A ternary runtime that loses to Q4_K_M on x86 is a finding about x86, not about ternary — but only if we measured it before claiming otherwise. This is why A1 is first. |
| The int8 head costs real quality | G2 is the arbiter and it is frozen. Ship it as a flag rather than a default if the cosine moves; the bytes/token win is not worth a lobotomized model. |
| No hardware with an energy counter | J/token stays unreported. The column being empty is the honest outcome, and estimating one would poison the single claim the project exists to make. |
| ARM turns out slow | Then say so. The NEON kernels were written against the same contract as the x86 ones and are correct; if they are slow, that is a kernel problem with a known shape, not a surprise. |
| Board arrives and timing does not close | Reduce lane count. The design scales roughly linearly with lanes and the roofline says the memory link binds long before the adder tree does. |
| The project drifts into being a CPU inference engine | The RTL parity gate runs on every push. Drift is detectable by construction, not by vigilance. |
Ground rules
- Every performance claim ships with a reproducible script.
tritsimis the source of truth. The CPU runtime and the RTL are both wrong until they match it.- Gates move only with an argued entry in 06-GATES.md.
- Energy is measured or absent, never estimated.
- Build in public. No emojis in project communications.