bf-compat — ByteFlow patch tokens × the dormouse loop (compat channel)
Lane: bfcompat. Worktree wt/bfcompat, branch wt/bfcompat. CPU-only
(flex/CPU backends — the GPU was running another job, and none of these gates
needs it), NaN firewall untouched, no merge to main, no 1M work.
Status: LANDED — this file is committed with the gates it names; the hash is the commit that carries it.
1. The seam
Section titled “1. The seam”Failure map: docs/reviews/byteflow-ab-2026-10-02.md. Gate sites:
crates/dormouse-core/src/config/validation.rs:308-400— theuse_byteflowrefusal block (7 arms, 4 aux weights,moe_topk, the window/shape Errs).crates/dormouse-train/src/byteflow.rs:82-127—check(), the train-side flags (--bf16,--graph-capture,--jepa-targets,--engram-ram,--rand-depth,--eval-depths,--stress,--opt ∉ {adamw, mix}), applied to BOTH byteflow dispatch modes.
The lane’s one change to that picture: use_byteflow + use_kda is no longer
a refusal. It runs the compat channel:
bytes [b,t] → ByteFlow front stage (embed + local encoder + coding-rate Top-K chunker + proj d_local→d_global) → patch latents z [b,K,d_global] → in_proj [d_global → d_model] → dormouse LoopBlock (KDA over K patch positions, `targets = None`) → out_proj [d_model → d_global] → ByteFlow decode_chunks (upsample + decoder blocks + byte head) → logits [b,t,256]BfChannel (crates/dormouse-core/src/bf.rs) owns the two pairing Linears
plus ByteFlow’s own params, with g_layers = 0 so net.global builds empty —
the byteflow-side checkpoint record stays byte-identical to the standalone
arm’s, and ByteFlowNet::forward (never called here) would refuse K=0 loudly.
The objective: the DECODER’S byte CE
Section titled “The objective: the DECODER’S byte CE”forward_channel (crates/dormouse-core/src/model.rs:450) runs the loop with
targets = None and returns rec = mean CE of the decoder’s [b,t,256]
logits against all T labels; aux = None. Two reasons, both load-bearing:
decode_chunksis the model’s only exit. A loss taken at K patch starts would leaveupsample_w,decoderandoutwith no gradient — a dead back stage behind a healthy loss curve (the8fa5d4cshape).- The held-out eval scores
[b,t,256]logits against all T bytes, so the train objective and the eval line are the same quantity. A K-position CE would not be.
The earlier draft of this document described a K-position CE through the
loop’s L_Rec. That contract was rejected during implementation for
reason 1; the byte CE above is what shipped.
2. Refusals lifted — exactly one
Section titled “2. Refusals lifted — exactly one”| refusal | state | gate |
|---|---|---|
use_kda with use_byteflow |
LIFTED — the pair this lane exists for; use_kda is now the one arm the block at validation.rs:308 allows |
byteflow_kda_smoke.rs::the_lifted_pair_resolves_and_the_channel_arms, preset_exec::byteflow_kda_executes |
Everything else refuses as before, and each is asserted by
byteflow_kda_smoke.rs::the_untouched_refusals_still_refuse:
- arms (
validation.rs:317-336):use_engram,use_mor,use_msa,use_gr,use_attnres,use_mhc,use_situ— 7 pairs, each asserted to name bothuse_byteflowand the arm (six throughresolve,use_msathroughvalidate— see §6.4). - aux (
validation.rs:337-355):jepa_weight,dspark_weight,aux_fb_weight,mor_bce_weight> 0 — 4 pairs. moe_topk > 0(validation.rs:356-363).- window/shape Errs (
validation.rs:364-399):max_seq_len > byteflow_max_bytes,byteflow_k_tokens == 0,eps2 <= 0,bins == 0are pre-existing; the lane added exactly ONE —byteflow_k_tokens > max_seq_len(validation.rs:378-384), because a preset shipping K > window would otherwise die insideencode_chunkson a shape assert that names no flag. (An earlier draft of this document claimed ak_tokens-power-of-two Err. That was never written; K is not required to be a power of two — the KDA chunker takes a partial last chunk, whichpreset_exec’s K=8 fixture now exercises.) - train-side flags (
byteflow::check, called directly by the same test —resolvenever sees these): 8 flags, each asserted to refuse, plus the default config assertedOkso the refuser itself cannot be a constant Err. - runtime seam:
forward_channel’sassert!(model.rs:471-479) — the model constructors do not callvalidate, so a hand-built config with an aux weight > 0 would otherwise have that term silently produce nothing (ADR-0019). Gated byan_aux_weight_cannot_be_silently_dropped.
Not lifted, deliberately not attempted: the 1M route (byteflow_max_bytes
stays 512, the max_seq_len > byteflow_max_bytes Err keeps its teeth, no
recurrent local window), the aux heads (they walk forward_full_state’s
[B,T,d_model] out_acc; the channel’s is [B,K,d_model] — width-aware aux is
a follow-up), Engram/MoR/GR/AttnRes/mHC/situ.
3. Dispatch and the arm line
Section titled “3. Dispatch and the arm line”train/src/lib.rs:958-990: byteflow::check(&cfg)? runs first for BOTH
modes, then if !bf::compat_armed(&dorm_cfg) { return byteflow::train_loop(..) }
— the standalone arm is unchanged; bf::compat_armed (bf.rs:90) is the ONE
list, and validate refuses the same arms by name, so a config reaching the
dispatch has already been ruled on. The duplicate compat_channel_armed in
train was deleted.
The step-0 line (COUNTED, lib.rs:985):
byteflow compat arm: front/back stages + dormouse loop (byteflow_g_layers replaced by the loop) over k={} patches of t={} bytes (patcher={}, opt={}) aux=none host-tables=noneopt= is build_optim‘s router, i.e. the same one as the dormouse path: the
stages’ 2-D Linears are Muon+ ColRow under --opt mix, not AdamW. An earlier
draft of this line ended byteflow-stages=AdamW(rest); that was false for the
channel and would have been an ADR-0011 mark naming the wrong thing.
The eval line carries bf={runs} — the channel’s own forwards during the
eval. bf=0 there means the eval scored a different network than the one
being trained (the §3.2 defect shape).
4. Gates (all CPU)
Section titled “4. Gates (all CPU)”| gate | what it pins |
|---|---|
dormouse-train/tests/byteflow_kda_smoke.rs::the_lifted_pair_resolves_and_the_channel_arms |
the pair resolves through resolve (it was a refusal) and the dispatch’s channel branch agrees with compat_armed |
…::one_step_trains_through_the_real_train_loop |
the REAL train_loop (not a hand-rolled step): 1 step, flex, ckpt bfk.bin written, snapshot carries both flags, log reaches step 1, probe::BF_CHANNEL >= 2 (the step forward + the eval’s own forwards — the number behind bf=), probe::KDA > 0 |
…::the_untouched_refusals_still_refuse |
6 arm pairs through resolve + use_msa through validate + the schema default refused, 4 aux pairs, moe_topk, 2 window Errs, 8 train-side flags through byteflow::check directly, and TrainCfg::default asserted Ok (so the refuser cannot be a constant Err) |
…::kda_group_receives_gradient_at_patch_input |
non-zero finite grads on gdn2.q_proj/k_proj, bf.in_proj, bf.net.proj, bf.out_proj, bf.net.upsample_w, bf.net.out — the decoder’s byte CE reaches BOTH the KDA arm (whose input arrived as a patch latent) and the back stage; also pins logits [2,32,256], aux.is_none(), BF_CHANNEL == 1, KDA == max_iter |
…::an_aux_weight_cannot_be_silently_dropped |
#[should_panic(expected = "carries no aux heads")] on the runtime assert! |
core/tests/preset_exec.rs::byteflow_kda_executes |
the shipped byteflow_kda preset resolves, validates, instantiates, runs 2 steps: KDA entered on patch input, BF_CHANNEL == STEPS, finite byte CE |
core/tests/preset_exec.rs::byteflow_executes |
the standalone preset still declares no arm (its dispatch never builds a DormouseModel) |
preset_exec::a_preset_that_declares_an_arm_it_never_enters_fails_here |
compat_armed joined the declared list: a channel that is built and never entered fails here |
train/tests/byteflow_smoke.rs::a_preset_arm_conflict_is_refused_loudly |
the standalone arm’s first refused arm is named (use_engram) |
Results — run on this worktree, 2026-10-03, CPU backend, commit below:
| command | result |
|---|---|
cargo test -p dormouse-train --test byteflow_kda_smoke |
5 passed, 0 failed (241.7 s) |
cargo test -p dormouse-train --test byteflow_smoke |
2 passed, 0 failed |
cargo test -p dormouse-core --test preset_exec |
15 passed, 0 failed (the cost test alone: 3816 s) |
cargo test -p dormouse-core -p dormouse-data -p dormouse-train --lib |
89 + 15 + 65 passed, 0 failed |
cargo test -p dormouse-core --test model_seam |
passed |
cargo clippy -p dormouse-core -p dormouse-data -p dormouse-train -p dormouse-cli --all-targets -- -D warnings |
clean |
cargo check -p dormouse-cli · cargo machete crates/ · python3 tools/check_doc_refs.py |
clean · clean · 0 missing paths |
cargo fmt … -- --check |
red, repo-wide, pre-existing — rustfmt 1.9 wants style_edition 2024 while the tree is committed in 2021 style, so it flags hundreds of untouched lines (graph.rs, routing.rs, optim.rs, …). ci.yml’s own comment records that this gate fails and skips clippy after it. Every file this lane created or edited is 0 diffs under the repo’s --style-edition 2021; the pre-existing diffs in byteflow_smoke.rs and train/src/lib.rs are not in this lane’s hunks and were left alone. |
Two reds found on the way, both traced to their cause before any fix: the
p150 pair (§6.1) failed at HEAD too — proven by stashing the lane and
rerunning — and dspark_aux_does_not_read_the_label_sequence (§6.2) went red
the moment the channel existed. No test was skipped, deleted or loosened to
get to green.
5. Fixture note (a capacity shrink, not an arm switch)
Section titled “5. Fixture note (a capacity shrink, not an arm switch)”preset_exec::fixture shrinks byteflow_k_tokens to 8 when
c.use_byteflow, because the byteflow presets ship K=128 over SEQ=32 fixtures
and K ≤ T is asserted in encode_chunks. This is a capacity field (the
max_seq_len class), which is why it is deliberately NOT in
execution_fields — that list is arm switches, and adding capacity there
would let a preset silently run at a different width than it ships. K=8 is
less than the attention arm’s chunk size of 16 (attention.rs:120), so
byteflow_kda_executes proves the partial-chunk path too; the trainer’s own
gate uses K=16 = exactly one full chunk.
6. Findings outside the lane
Section titled “6. Findings outside the lane”Two were pre-existing at HEAD and are fixed here (both proven pre-existing by running the same test at HEAD, message and all):
configs/p150.tomlshipped withoutuse_byteflow = false— it madepreset_exec::p150_executesand…::a_preset_that_declares_an_arm_it_never_enters_fails_herered. The schema default flipped to the winning byteflow net on 2026-10-02 with the rule “every dormouse preset listsuse_byteflow = falseexplicitly”; p150 was the only one of thirteen that did not, sopreset(p150)panicked insidevalidate. One line + a comment.aux.rs::mini()(the DSpark regression config) had nouse_bytefloweither, so withuse_engramdefaulting on it armed the channel andforward_channel’s auxassert!fired —aux::tests::dspark_aux_does_not_read_the_label_sequenceis the test §3.3 calls the DSpark fix’s gate, and it went red the moment the channel existed. One line, the same one-linerloop_block.rs:1282,1500already carry. This one is caused by this lane (before it,use_byteflowwas unread insidecore), which is why it is fixed and not filed.
Three are reported, not fixed:
--seq-len < byteflow_k_tokenspanics insideencode_chunksnaming no flag. Nothing tiesTrainCfg.seq_lentobyteflow_k_tokens;validatecompares the CONFIG’smax_seq_lenbut the trainer’s--seq-lenis a separate field. The standalone arm bites identically — pre-existing, not introduced here. Cheapest fix: one Err inbyteflow::check(and/orresolve) naming both fields.--set use_msa=trueis not a config key (config/override.rs:164-172lists every otheruse_*arm). The field exists in the schema andvalidaterefuses it withuse_byteflow, so the only way to turn MSA on from the CLI is a preset file. Pre-existing; the refusal gate asserts this one atvalidatedirectly instead of throughresolve.DormouseConfig::default()does not passvalidate. The default is the schema default —use_byteflow = true(schema.rs:111) anduse_kda = true(:308) anduse_engram = true(:331) — which the block this lane edited refuses. So “a flat preset missing the field gets the winning net” (schema.rs:654) is only true of a net that also has to switch its arms off;configs/byteflow.tomldoes exactly that (use_kda = false,use_engram = false), but nothing says so at the default. Pre-existing since the 2026-10-02 flip, LOUD (resolve names every offending arm), so it is a schema-doc gap rather than a defect class — and it is the reasonaux::mini()above armed a channel at all. Changing a default is an owner decision; not taken here.- Aux heads on the channel (width-aware
out_acc), then re-look at the aux refusals; and Engram/MoR/GR/AttnRes on the channel. Same method, none attempted.