Conversation
…ntical Generation re-ran the whole prefix for every token (gen_loop's O(n^2)); stdlib/infer_kv.rail adds the KV-cache step: one new row per block, K and V for every earlier position from a preallocated cache, O(n). The contract is bit-parity, not approximation: every loop matches the accumulation order of the tensor.rail CPU loops, causality plus the mask's exact-zero underflow does the rest. tools/infer/kv0_gate.rail enforces it -- two model configs, both paths interleaved, every step's FULL probability row compared: max |p_full - p_kv| must be exactly 0.0. The gate earned its keep on the first run: 5.1e-9 divergence, root cause tensor_transpose GPU-dispatching K through f32 on the reference side only. Both paths now force the CPU loops (the documented .no_gpu reference config), and the gate's header records the catch. No arena_reset anywhere near the loops (2026-04-30 corruption). Suite 189/189; compiler untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EzjMXh1wxgasCH81MJfTCk
A pure-Rail HTTP server (socket.rail, fleet_agent_v3 pattern) serving greedy generation through the KV0 incremental path. The scheduler is the accept loop: batch-1, serial FIFO, by construction. Every /generate appends a hash-chained ledger record and reports ISL/OSL/TTFT/TPS in the response; /ledger/verify recomputes every record hash and walks the chain -- check-me, not trust-me. Model is a plug: --prefix loads any v3 checkpoint; without one a deterministic demo model exercises the full contract (no v3 checkpoints survive on disk -- flywheel-era weights are gone). s0_gate.sh boots it and tests the contract: identical request twice must return identical bytes; a tampered ledger must fail verification; a garbage request must not kill the server. All eight checks green. Two stdlib-level catches on the way: - recv_http_request returned at the header terminator, so any client that writes headers and body separately (curl does) lost its POST body; it now reads Content-Length bytes of body, as its own comment had promised. - "\r" in Rail source is a literal backslash-r (trap #8); the CR strip in request parsing stripped nothing until synthesized via char_from_int 13. Suite 189/189; fleet_agent_v3 still compiles against the socket change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EzjMXh1wxgasCH81MJfTCk
POST /generate_stream sends each token the moment it exists -- close-delimited, no chunked encoder needed, curl -N watches it happen. Streaming is transport, never content: the gate proves sha256(streamed bytes) == the output_sha256 that /generate reports for the same request, the stream is byte-deterministic across calls, and first-byte time is a fraction of total time (it is actually live). The ledger records streamed generations identically. Two catches: - the single-char strings id_char returns hit the runtime-string -> foreign char* marshaling trap in send(): right length, header byte 0x09 on the wire instead of the character. Ledger hashes exposed it instantly (collected text hashed correctly while the wire bytes didn't). Cat-wrap before send is the workaround, documented inline. - rail_native run execs the server as a child, so a gate that kills only the launcher pid leaves an orphan holding the port and every later run talks to stale code. The gate now pkills by binary path, before and after. Suite 189/189. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EzjMXh1wxgasCH81MJfTCk
A request that sends "cache": "reuse" opts into the single resident slot; everything else always runs cold. The declaration rides in the ledger canon -- cache behavior is part of the attested transcript, never an ambient optimization -- and the response reports hit/cold/off. Reuse is common-prefix, both directions: cache rows at positions beyond the current one are ignored by the causal attention loops (j <= pos), so a cache longer than the prompt (a re-ask) or diverging later (an edit) still safely reuses everything up to the divergence. The slot records prompt + gen[:-1] -- the final generated token's K/V row is never written, so it is never claimed. Gate: first declared run cold, second a hit, and hit bytes == cold bytes == undeclared bytes -- the cache may never change the output. Fourteen checks green; suite 189/189. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EzjMXh1wxgasCH81MJfTCk
Two additions that turn the serving contract into fleet mechanisms: - agree_check.sh sends the identical request to two serve_kv endpoints and compares output_sha256 -- any disagreement between machines running the same model is an alarm for the fault class no health check sees (silent corruption, weight drift, diverged binaries, a changed libm). Proven live before landing: Mini (M4 Pro, macOS 26.3) and Studio (M1 Ultra, 26.4.1) AGREE byte-for-byte through the entire serving pipeline. - /ledger/head answers the chain head Ed25519-signed (pure-Rail ed25519, seed = sha256 of a fleet-style key file). The head commits to every record beneath it, so the signature covers the entire serving history. The server runs ed25519_verify on its own signature before answering -- verify:1 is checked, not asserted. Without a key it answers signed:false, honestly. Gate is now fifteen checks; suite 189/189. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EzjMXh1wxgasCH81MJfTCk
Ports the attested float layer into this repo's dylib and lands the engine plus its gates, so the determinism claims can be checked by anyone with a Mac rather than taken on faith. That felt like the minimum bar for a determinism result. What is here: incremental decode bit-identical to a full forward pass on a real 138M model (max diff exactly 0.0 over 98304 hidden values and the full 16384-logit row); a chunk of K tokens bit-identical to K sequential steps with all 32 K/V caches included; and n-gram speculative decoding proven to change the schedule and never the output, 2.0x fewer forward passes on repetitive input with byte-equal tokens. Verified on M4 Pro (macOS 26.3), M1 Ultra (26.4.1) and base M1 (26.5.1), each compiling its own dylib and kernels. The README states the size of the claim honestly: this is replayability, not verifiability, because a checker spends what the producer spent; batch-1 is a choice rather than a requirement, and the keystone gate is really a batch-invariance proof that has not been spent on throughput; the guarantee is greedy-only; and LLM-42 got to the same observation from the opposite direction. The narrow contribution is the numerical half: making chunk-equals-sequential true bitwise, which is what production engines lack. rsqrt_probe.rail documents the one real numerics finding: Metal's rsqrt differs from 1/sqrt on 1161 of 4096 values by about an ulp, though both agree across all three machines. LayerNorm now uses IEEE-exact 1.0f/sqrt anyway. Suite 189/189 with the ported dylib. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EzjMXh1wxgasCH81MJfTCk
… tree An outside review took the repo apart. The worst finding was mine from yesterday: the parity gates could pass on a dead engine. Max-difference alone cannot fail on all-NaN (IEEE says every comparison with NaN is false, so the running max stays 0.0) nor on all-zeros (two dead paths agree perfectly). A determinism artifact whose gates do not gate is worse than no artifact. Fixed by counting finite nonzero values before comparing, and proven by falsification: with the weights file moved away, the gate that used to report `max diff 0.0, argmax 0 == 0, PASS` now reports `hidden 0/98304 live` and fails. ng0_spec compared min(n0,n1) tokens, so a speculative path emitting fewer tokens passed on the prefix; it now requires equal length. Also from the review: the serving loop forked a Perl interpreter three times per request inside a loop whose whole claim is being pure Rail, and it meant ttft_ms partly measured Perl's startup. tgl_now_ms was in the dylib the entire time. s0_gate still 15/15 with the native timer. Honesty repairs, all earned: the speculation table's cycle6 row accepts nothing, runs the same code path as plain greedy, and is now labelled the control it always was. The README described five of ten files and omitted the server, its gate, the agreement checker, and the one gate that needs no weights at all; kv0_gate is now the documented starting point. The rsqrt section claimed the portability question was never a problem without mentioning the apparent divergence that motivated the probe, which turned out to be a stale file and a hash check that compared the wrong file. That story is now told, because it is the argument for binding kernel-source, OS, and Metal-version hashes into a receipt, which does not exist yet. Repo-level: docs/STATUS.md had drifted on three of three numbers, so `tools/verify/check.sh` (the one command VERIFY.md asks a stranger to run) failed on HEAD. Regenerated. Test counts said 187 in the README and 182 in VERIFY.md against a real gate of 189. The license was stated three different ways: LICENSE grants production use except competitive hosting and converts to MIT 2030-03-14, while README said "free for non-production use" and CONTRIBUTING said Apache 2.0 on a different date. Both now match LICENSE, and the BSL choice is finally explained. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EzjMXh1wxgasCH81MJfTCk
The one command VERIFY.md asks a stranger to run has been failing on a correct tree. gen_status stamps every run with the wall clock, and the check then did `git diff --exit-code`, so the file differed the instant it regenerated no matter what the numbers said. A permanent red line on the repo's own self-audit, which is the worst possible place for one. Now diffs everything except the "Generated by ... on DATE" line, so it fails on a drifted FACT and passes on a fresh timestamp. 5/5 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EzjMXh1wxgasCH81MJfTCk
Every gate in tools/infer proves something about integers. That makes the determinism claims checkable and the engine's output unreadable: nobody can see language in a list of ids. This is the bridge, using the real tokenizer the 240M base run was trained with (173 base bytes plus 15,404 merges, vocab 15,577). Encoding is the slow obvious algorithm on purpose. Every merge is applied in learn order as one left-to-right non-overlapping pass, because that is what stdlib/bpe.rail did when this vocabulary was trained. A faster encoder producing different ids would be worse than no encoder: the model would receive tokens it never saw, output would become noise, and every number downstream would look fine while measuring nothing. Two independent checks, because each is blind where the other sees. tok0_gate.sh compares Rail's ids against bpe_replica.py, the Python implementation the corpus pipeline used, over seven cases picked for what breaks byte-level BPE: multi-byte UTF-8, digits, deep runs, empty input. Proven able to fail by skipping a single merge in the Rail encoder, which four of seven cases catch. That differential check is blind to the input, though. Both sides read whatever is in TOKDIR, so truncating the merges file by 100 lines left all seven cases reporting ok. Hashes catch what differential tests cannot, so the gate now verifies the merges against the run's recorded tokenizer.sha256. It also pins the alphabet hash, which that recording never covered: the alphabet is half the tokenizer, and changing one byte of it shifts every id while the recorded hash stays valid. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EzjMXh1wxgasCH81MJfTCk
…mits rl0_gate.rail asks where decode actually spends time, and answers it with two measurements instead of one. The engine's throughput alone says nothing; it only means something next to what this machine can actually do, so the gate measures the memory ceiling with a coalesced streaming kernel rather than quoting a spec sheet. A spec figure describes ideal conditions no real kernel meets and flatters every engine equally. The answer was not the expected one. Batch-1 decode reads 0.45 GB of weights per token and should be memory-bound, but the engine reached only about 6% of measured bandwidth. So the gate measures the other term: 288 EMPTY dispatches cost 24-34 ms while a real decode step cost 36 ms. Eight to nine tenths of every token was the submission boundary. Every tgl_* call built its own command buffer, encoded one dispatch, committed, and waitUntilCompleted, and decode issues 18 dispatches per block across 16 blocks. The engine was not reading memory. It was waiting. That verdict overrides the plan it was written to inform: quantization cuts bytes read per token, and bytes read per token was never the constraint. tgl_batch_begin/tgl_batch_end encode into one shared command buffer and commit once. The kernels, arguments and order are untouched, so only the moment the CPU stops to listen changes. 27.6 -> 131 tok/s, a 4.75x speedup, with the decode checksum bit-identical and all three determinism gates unchanged: kv138 still hashes 092af18f2b576bd5 with max diff 0, keystone still 0 divergent of 34, speculation still matches greedy token for token. Two things the gate caught in itself along the way. It failed its own repeatability check at 16.7% spread between identical bandwidth runs, and again at 20.6%. Both were fixed by making the estimator honest rather than the threshold loose: best-of-N, since contention can only ever slow a run, and the two batches interleaved rather than sequential, since this machine runs a GPU beacon at ~9fps and sequential batches turn drift in machine load into fake disagreement. Spread is now 0.7-2.2%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EzjMXh1wxgasCH81MJfTCk
The gate was diagnosing and grading the unbatched path, which is the one nobody should run now that batching exists. A gate that keeps measuring the old strategy keeps recommending the old fix long after it landed, so the verdict and the RESULT line now follow the batched numbers: 27% of measured bandwidth at 133 tok/s. The unbatched figure is still printed, as the before to the after. README gains the roofline result and the tokenizer, including what each one is not: the tokenizer reproduces the 240M run's vocabulary exactly, but the weights these gates run on are a 138M initialisation, so text in and ids out both work while language out would need that checkpoint ported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EzjMXh1wxgasCH81MJfTCk
The pre-push leak guard caught it, correctly. tok0.rail defaulted --tokdir to one machine's home directory, which in a public repo is both a leak and a claim that a private training artifact ships here. It does not. The path now comes from tok0_gate.sh, where a shell can expand $HOME; Rail's shell() inherits no environment, so it cannot do this itself. Running without --tokdir prints what to pass instead of failing on a missing file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EzjMXh1wxgasCH81MJfTCk
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Twelve commits building a transformer inference engine where the same
request produces the same bytes, on three different Apple GPUs, checked by
tests that demand exact equality rather than closeness. Written in Rail,
running on Metal, no CUDA, no PyTorch, no Python in the inference path.
Full README with the claims and their limits:
tools/infer/README.md.The claims, each with a gate
kv138_gate.railrequiresmax |full - incremental| == 0.0across all98,304 hidden values and the full 16,384-wide logit row.
keystone.
ng0_keystone_gate.rail: 48 tokens both ways, independentbuffer sets,
divergent comparisons: 0 (of 34).ng0_spec.raildoes n-gram draft-and-verify with no draft model and nosecond set of weights: 2.0x fewer forward passes, byte-equal output.
/ base M1, each compiling its own dylib and kernels, same hash.
The gates can fail
An outside review found they could not. Max-difference alone is blind: an
all-NaN run leaves every running max at 0.0 because IEEE says every
comparison with NaN is false, and an all-zero run agrees perfectly while
proving nothing. Every parity gate now counts finite nonzero values first
and fails if the tensor is mostly dead. Verified by deleting the weights
file: the gate that used to report
max diff 0.0, PASSnow reportshidden 0/98304 liveand fails.The roofline changed the plan
rl0_gate.railmeasures the memory ceiling with a coalesced streamingkernel rather than quoting a spec sheet. Batch-1 decode reads 0.45 GB of
weights per token and should be memory-bound. It reached 6% of measured
bandwidth. So the gate measured the other term: 288 empty dispatches cost
24-34ms against a 36ms decode step, because every
tgl_*call built its owncommand buffer, committed, and waited.
tgl_batch_begin/tgl_batch_endencode the step into one command buffer.Kernels, arguments and order are untouched, so only the moment the CPU stops
to listen changes.
4.8x, decode checksum bit-identical, all three gates unchanged. That is
what the gates are for.
The gate also failed its own repeatability check twice (16.7%, then 20.6%
spread between identical bandwidth runs) and was fixed by improving the
estimator rather than relaxing the threshold: best-of-N, since contention
only ever slows a run, and interleaved comparison batches, since this
machine runs a GPU beacon at ~9fps and sequential batches turn drift in load
into fake disagreement. Spread is now 0.7-2.2%.
The ids become language
tok0.railis the byte-level BPE the 240M base run was trained with: 173base bytes, 15,404 merges, vocab 15,577. Encoding is deliberately the slow
obvious algorithm because that is what produced these ids; a faster encoder
giving different ids would feed the model tokens it never saw.
tok0_gate.shchecks it two independent ways, because each is blind wherethe other sees. It compares against
bpe_replica.pyover seven cases pickedfor what breaks byte-level BPE, and skipping one merge is caught by four of
seven. But that check cannot see its own input: truncating the merges file
by 100 lines left all seven reporting ok. So it also verifies the merges
against the run's recorded
tokenizer.sha256and pins the alphabet hash,which that recording never covered even though the alphabet is half the
tokenizer.
Being precise about the size of this
Replayability, not verifiability. Verification costs what generation
cost: a checker needs the weights, matching hardware, and the same time.
That makes it a fraud-proof primitive, not a succinct proof to a stranger.
zkML and TEE attestation give a verifier an asymptotic advantage; this does
not.
Batch-1 is a choice, not a requirement. The keystone gate is in fact a
batch-invariance proof; this engine has not spent it on throughput yet.
Greedy only, and prior art exists and is good (LLM-42 runs the same
argument in the opposite direction). The honest contribution is the
numerical half: making chunk-equals-sequential true bitwise on commodity
hardware.
Verification
190/190 tests, tokenizer gate 9/9, all three parity gates pass, leak guard
clean. Start with
kv0_gate.railandrsqrt_probe.rail, which need noweights and no downloads.