Skip to content

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.

Failure map: docs/reviews/byteflow-ab-2026-10-02.md. Gate sites:

  • crates/dormouse-core/src/config/validation.rs:308-400 — the use_byteflow refusal 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.

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:

  1. decode_chunks is the model’s only exit. A loss taken at K patch starts would leave upsample_w, decoder and out with no gradient — a dead back stage behind a healthy loss curve (the 8fa5d4c shape).
  2. 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.

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 both use_byteflow and the arm (six through resolve, use_msa through validate — 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 == 0 are 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 inside encode_chunks on a shape assert that names no flag. (An earlier draft of this document claimed a k_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, which preset_exec’s K=8 fixture now exercises.)
  • train-side flags (byteflow::check, called directly by the same test — resolve never sees these): 8 flags, each asserted to refuse, plus the default config asserted Ok so the refuser itself cannot be a constant Err.
  • runtime seam: forward_channel’s assert! (model.rs:471-479) — the model constructors do not call validate, so a hand-built config with an aux weight > 0 would otherwise have that term silently produce nothing (ADR-0019). Gated by an_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.

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=none

opt= 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).

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.

Two were pre-existing at HEAD and are fixed here (both proven pre-existing by running the same test at HEAD, message and all):

  1. configs/p150.toml shipped without use_byteflow = false — it made preset_exec::p150_executes and …::a_preset_that_declares_an_arm_it_never_enters_fails_here red. The schema default flipped to the winning byteflow net on 2026-10-02 with the rule “every dormouse preset lists use_byteflow = false explicitly”; p150 was the only one of thirteen that did not, so preset(p150) panicked inside validate. One line + a comment.
  2. aux.rs::mini() (the DSpark regression config) had no use_byteflow either, so with use_engram defaulting on it armed the channel and forward_channel’s aux assert! fired — aux::tests::dspark_aux_does_not_read_the_label_sequence is the test §3.3 calls the DSpark fix’s gate, and it went red the moment the channel existed. One line, the same one-liner loop_block.rs:1282,1500 already carry. This one is caused by this lane (before it, use_byteflow was unread inside core), which is why it is fixed and not filed.

Three are reported, not fixed:

  1. --seq-len < byteflow_k_tokens panics inside encode_chunks naming no flag. Nothing ties TrainCfg.seq_len to byteflow_k_tokens; validate compares the CONFIG’s max_seq_len but the trainer’s --seq-len is a separate field. The standalone arm bites identically — pre-existing, not introduced here. Cheapest fix: one Err in byteflow::check (and/or resolve) naming both fields.
  2. --set use_msa=true is not a config key (config/override.rs:164-172 lists every other use_* arm). The field exists in the schema and validate refuses it with use_byteflow, so the only way to turn MSA on from the CLI is a preset file. Pre-existing; the refusal gate asserts this one at validate directly instead of through resolve.
  3. DormouseConfig::default() does not pass validate. The default is the schema default — use_byteflow = true (schema.rs:111) and use_kda = true (:308) and use_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.toml does 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 reason aux::mini() above armed a channel at all. Changing a default is an owner decision; not taken here.
  4. 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.