memra_engine/glm_spec.rs
1//! glm5_next T-parallel speculative verify: the acceptance/rollback machinery that turns the
2//! native MTP draft head (`mtp_head_forward_mla_cached`, lane/glm5-mtp-remint) into served
3//! speculative decoding on the HyperConnections trunk.
4//!
5//! THE WALK-REUSE DECISION, stated once and load-bearing everywhere below: the verify of K
6//! drafted tokens is a t=K+1 walk over the SAME trunk, and it rides the BATCHED-DECODE walk's
7//! kernel classes (`hyper_batch_range_decode`, lane/glm53-batched-decode), NOT the prefill /
8//! prime walk. Why:
9//!
10//! * The batched walk's row-parallel ops are per-row BIT-EXACT vs the isolated t=1 decode
11//! step at every width in `1..=hyper_batch_cap()` (= `PRIME_MIN_T - 1` = 15, the shexp
12//! decode-exact knee, measured in `batched-decode-gate/31-KNEE-b16-forced.log`): the hc
13//! glue is block-per-token, the hc-mix GEMM runs per-row m=1 (`hyper::pre_exact`, the
14//! lt_ndep law), the MoE FFN's router/experts/shexp are per-row programs below
15//! `PRIME_MIN_T`, and the lm_head is `matmul_decode_exact`. That per-row exactness is
16//! what makes spec-vs-plain BYTE IDENTITY achievable at all.
17//! * The prime walk is a different numeric class: `hyper::pre` batches the mix GEMM
18//! (cuBLASLt n-dependent reduction), the FFN takes the prefill dispatch
19//! (`moe_ffn_il_prefill` / grouped GEMMs), and KDA's prefill conv arm reads the chunk
20//! window instead of the decode ring program. None of those are per-row bit-identical
21//! to the t=1 decode chain, so a prime-walk verify could never pass a byte-identity
22//! gate against plain decode.
23//! * The knee bounds K: K+1 <= 15, i.e. K <= 14 — the DFlash2 probe's K<=7 drafter and
24//! upstream's 5/7-draft MTP configs fit with margin.
25//!
26//! The ONE difference from `hyper_batch_range_decode`: the batched walk runs B independent
27//! sessions (each row -> its OWN cache at its own single position); the verify walk runs
28//! K+1 SEQUENTIAL positions of ONE session, so the mixers chain state row -> row through the
29//! one cache (KDA: the t=1 recurrent step per row, exactly the serving decode program; MLA:
30//! the t=1 `mla_attn_cached` append+attend per row, so row r attends rows `0..pos0+r+1` —
31//! causal within the drafted block by construction). This is the "hyper rows-walk with
32//! causal verify appends" the batched-decode lane's standing refusals named as missing.
33//!
34//! VERIFY-ROW BATCHING (lane/glm5-verify-batch, 2026-08-30 — the flip re-battery's named
35//! flip condition: "the verify walk must stop paying one plain-step per row", ~24-26 ms/row
36//! measured): `MEMRA_GLM5_VERIFY_BATCH` (default ON) restructures the mixer walk PER LAYER
37//! while keeping the sequential contract exactly where the math demands it. Each KDA layer
38//! runs ONE t=K+1 `kda_core` call — projections/gates/conv batched through the decode-exact
39//! matmul classes (`matmul_rows_exact`; the bf16 tcols twin reads each weight ONCE for all
40//! t rows) with the recurrence SEQUENTIAL INSIDE one `memra_kda_scan_s128` launch (the
41//! in-kernel T-loop over register state IS the chained t=1 program). Each MLA layer runs
42//! ONE t=K+1 `mla_attn_cached_rows_exact` call (per-query causal kpool selection + gathered
43//! attention by construction). Rollback on the batched arm: conv ring = pre-round snapshot
44//! + re-roll(T=keep) over stolen raw rows; ssm = ONE scan replay at T=keep from the
45//! pre-round snapshot over the stolen batched inputs (`kda::KdaRowsStash`). `0` = the
46//! per-row walk below, byte-for-byte — the rollback seam. Per-row byte identity vs the
47//! plain tape is held by this file's standing batteries running the batched arm, plus the
48//! kernel bit-gates in `tests/glm5_verify_batch_gpu.rs`.
49//!
50//! ROLLBACK (the hard part, per the engine survey's upstream reading — vLLM keeps
51//! num_spec+1 KDA state columns and commits the last accepted one; SGLang's ReplaySSM keeps
52//! an input ring and replays the accepted prefix):
53//!
54//! * KDA recurrent state: SNAPSHOT + SCAN-INPUT REPLAY (lane/glm5-loop-port port 3 —
55//! the GdnStash/ReplaySSM diet this doc used to name as the follow-up, landed). The
56//! walk clones the resident ssm state ONCE per layer per round (before row 0) and
57//! STEALS each row's scan-input buffers (`kda::KdaScanInputs`, ~160 KB/row/layer,
58//! zero copies — the step allocated them either way); accept-j REBUILDS the state by
59//! re-issuing rows 0..=j's original t=1 `memra_kda_scan_s128` launches from the
60//! snapshot (`kda::kda_scan_replay`) — byte-identical to the retired per-row clone
61//! by construction, since each replay is the very launch that produced it. Full
62//! accept keeps the resident state, no work. Memory, stated: one glm5_next KDA state
63//! is 64 heads x 128 x 128 f32 = 4 MiB, x34 KDA layers = 136 MiB per round (was 136
64//! MiB PER COLUMN — ~0.95 GiB of transient clones at K=7, retired to ~136 MiB + K x
65//! ~160 KB x 34 of stolen stash). The conv ring stays per-row cloned (288 KiB/row/
66//! layer, 1.4% of the ssm plane — not worth a replay arm).
67//! * MLA latent rows: TRUNCATE (append-only, position-addressed): `len = snap + keep`,
68//! device mirror in lock-step.
69//! * kpool index planes: the tail ring drains IN-CALL (`mla_kpool_indices`), so pool keys
70//! over drafted rows may FINALIZE during the verify walk. Rollback clamps
71//! `index_pools_ready` via `truncate_index_pool_keys` (the clamp the field's own doc
72//! demands of "the same code that shortens len"); the next call rebuilds keys for the
73//! re-appended rows from `[ready, len/pool)`. The residency tripwire in
74//! `mla_kpool_indices` fails LOUDLY if any rewind forgets this — that tripwire is the
75//! red arm of the rollback gate.
76//! * MTP draft plane (il = n_trunk): len reset by the loop (LANE.md contract: "rollback =
77//! plane len reset"), same pool-key clamp.
78//!
79//! Gate: `tests/glm5_tparallel_verify_gpu.rs` — accept-j-then-continue byte identity vs the
80//! never-drafted sequential path for every j in 0..=K, red-proven with a stale-KDA-state
81//! mutation and a pool-key-finalized-past-j mutation; plus end-to-end spec-vs-plain greedy
82//! tape identity at K=1..7 with forced-rejection positions, red-proven by disabling
83//! rollback. The SERVED shape (worker-sized bursts over one [`Glm5SpecSession`], state
84//! carried across burst boundaries, sampled twin, EOS, receipt log red/green) is gated by
85//! `tests/glm5_spec_session_gpu.rs`.
86//!
87//! SAMPLED ACCEPTANCE (landed, lane/glm5-spec-routing 2026-08-30): greedy
88//! longest-matching-prefix below is the byte-deterministic instrument
89//! (greedy-is-the-instrument law). The sampled arm applies memra's existing spec sampled
90//! contract — `spec::SpecSampling` with the rejection-sampling accept walk
91//! (`u_j * q_j(x_j) < p_j(x_j)`, host Philox4x32-10 stream tag 0xFFFF_FFFE via
92//! `spec::host_u01`, residual resampling on the first rejection) — the same contract
93//! `generate_spec_inner2`'s MEMRA_SPEC_TEMP>0 route and the dspark sampled-admission walk
94//! consume. It plugs in at exactly one seam (`glm5_sampled_accept`, the accept rule over
95//! the verify logit rows, with the draft chain drawn from the SAME filtered distribution
96//! the q gather reads); nothing in the walk or the rollback changes. Philox counters live
97//! ON the [`Glm5SpecSession`] so randomness never repeats across serve bursts (the
98//! session-continuity law; burst-split invariance is pinned by `glm5_spec_session_gpu`).
99//! PENALIZED requests (greedy and sampled) refuse UNLESS `MEMRA_SPEC_PENALTY=1`
100//! (lane/spec-exclusions-20260902, doc on [`glm5_spec_penalty_on`]): under the arm every
101//! verify round penalizes a copy of the target rows over the per-row evolving session
102//! window with the host sampler's exact arithmetic and the accept reads only that copy; the
103//! worker's admission keeps penalized requests plain while the arm is dark.
104//!
105//! FR-SPEC VOCAB MASKING (owner addition, 2026-08-30 — the house spec recipe, the q38 way):
106//! the loop consumes the existing `MEMRA_FRSPEC_TRIM` contract, no new flag. The loader
107//! already reaches this head: the trim match in `hybrid.rs` consumes the embedded head
108//! regardless of arch, `frspec_trim_own_head_name(n_trunk)` misses (glm5_next ships no
109//! private MTP lm_head) and the gather falls back to the trunk `output.weight` /
110//! `token_embd.weight` — which for glm5_next is EXACT BY CONTRACT, not merely by tying:
111//! the MTP block projects through the trunk lm_head (LANE.md; the draft gate pins
112//! `shared_head_head.is_none()` untrimmed). With a ranks artifact loaded,
113//! `mtp_head_forward_mla_cached` already projects through the gathered rows (its head is
114//! `shared_head_head.unwrap_or(trunk)`), so the DRAFT logits arrive `[n_ranks]`; this
115//! loop's seam is the remap: every draft argmax is a RANK id and maps through `d2t` back
116//! to the true vocab BEFORE it is drafted, chained (e_tok drives the embedding gather),
117//! or verified. THE VERIFY WALK STAYS FULL-VOCAB AND UNTOUCHED — a trimmed draft can only
118//! change WHICH tokens get drafted, never how they verify; that invariant is the whole
119//! design (q38's measured skipped-remap defect was 0/248 acceptance with every exactness
120//! gate green — silent, which is why the gate below makes it loud). Rank artifacts for
121//! glm5 are an INPUT DEPENDENCY: the corpus mint (SXC pools through GLM's tokenizer,
122//! per traffic class, the q38 plain-text format) is the owner's CPU-only lane; the
123//! self-trim d2t arm needs no external artifact and lands here first.
124//!
125//! PPN (lane/glm5-ppn-verify, 2026-08-30): the verify walk owns its stage split exactly as
126//! the batched decode walk does — `glm5_verify_rows_ppn` mirrors
127//! `decode_step_batch_hyper_ppn` (per-stage engine, per-stage pos_rows, ONE
128//! `[t, streams, n_embd]` boundary payload per cut; row chaining is per-LAYER through the
129//! one cache, so a straight layer-range split preserves it exactly). Rollback restores each
130//! stage's layers through that stage's engine on its stream; the MTP block, its latent
131//! plane and every draft-chain/accept-side op ride the LAST stage's engine
132//! (`glm5_head_engine` — where the loader puts the lm head and `pp::new_cache*` puts the
133//! trailing MTP plane), so the h_seed carrier never bounces devices. Gate:
134//! `glm5-spec-ppn-gate` (the tparallel battery under the split, stages=2 and 3, red-proven;
135//! the cross-device twin is the box arm). Worker admission bounds sharded placements to the
136//! GATED stage set (`glm5_sharded_placement_admits`, worker.rs) — everything else stays
137//! fail-closed by name.
138//!
139//! DRAFT SOURCE SEAM (lane/glm5-dflash-draft-src, 2026-08-30): the session's drafts come
140//! from ONE of two sources, selected at load and pinned for the session — everything from
141//! the verify walk on (accept, rollback, commit, receipts, K policy) is SHARED and
142//! source-blind, which is where the exactness invariant lives (a draft source can only
143//! move acceptance, never output):
144//!
145//! * `NativeMtp` (existing): the embedded NextN head chains K drafts through
146//! `mtp_head_forward_mla_cached`; requires `MEMRA_GLM5_MTP=1`.
147//! * `Dflash2` (`MEMRA_GLM5_DFLASH=<dir-or-hf-spec>`): the pinned
148//! incoai/GLM-5.3-Flash-DFlash2 block-diffusion drafter (owner holds WRITTEN APPROVAL
149//! from the DFlash2 owners, 2026-08-30, for use beyond probe/eval). It REUSES the
150//! shipped q38 DFlash2 machinery verbatim (`DflashDraft`: `ctx_features` ->
151//! `ingest_ctx` -> `forward_round` -> `dflash2_propose_*`, mask-fill harvest, selector
152//! walk); the ONE glm5-specific input is the drafter's measured feature contract — the
153//! STREAM-MEAN (`hc_contract`) of the COMPLETED trunk layer output at the drafter
154//! config's `target_layer_ids` (plan layers 5,14,24,33,42 on the real artifact), the
155//! exact definition the probe's `MEMRA_TRACE_LAYER_ROWS` capture seam banked 0.73
156//! acc@1 / 3.06 tokens-per-cycle against
157//! (research/glm53-flash-bringup-20260827/dflash2-probe-20260829/RECEIPTS.md). The
158//! features flow through [`crate::cache::HcTapSink`], a HOST sink filled by the hc
159//! prime walks and this file's verify walk (host-staged so a ppN split needs no
160//! cross-device tap plumbing; the drafter itself runs on the HEAD engine, where the
161//! trunk lm_head it projects through lives). THE NATIVE MTP HEAD IS NOT LOADED for
162//! this source (the q38 pattern — a full MoE trunk layer of VRAM back); the plan's
163//! trailing MTP cache plane still allocates (plan-structural, ~`ctx * latent_width`
164//! f32 per declared block — named cost, not forked). Sampled route: the drafter's
165//! selector proposal records its true q (`DsparkDraftSample::Selector`) and the accept
166//! rides the SAME `dspark_accept_sampled` rejection walk the q38 serve route ships,
167//! with this session's Philox counters (`uctr` selector/accept draws, `sctr`
168//! bonus/residual) so randomness never repeats across bursts. K is bounded by the
169//! drafter block (K <= block_size-1 = 7): the worker clamps, the burst refuses loudly.
170//! Selection receipt: boot logs `[glm5-spec] draft source = native-mtp` or
171//! `[glm5-spec] draft source = dflash2 @ <sha8>`; both flags off = plain serving
172//! (fail-closed warn). `MEMRA_GLM5_DFLASH_GATE_RED=tap-shift` is a GATE INSTRUMENT
173//! (never a serving flag): it shifts every tap layer +1 to red-prove that a wrong
174//! feature input collapses acceptance while the tape stays byte-identical.
175//!
176//! SERVING EXPOSURE (lane/glm5-spec-routing, 2026-08-30): `MEMRA_GLM5_SPEC` (default OFF,
177//! FLAGS.md row) is the ONE master flag — it routes `generate_spec` here for hc trunks
178//! with a loaded MTP head AND arms the worker route (`glm5_spec_capable` +
179//! `step_glm5_spec` driving [`Glm5SpecSession`] bursts). OFF = the named `refuse_hyper`
180//! refusal and zero `[glm5-spec]` log lines, byte-identical serving. The MTP_SPEC
181//! capability manifest remains deliberately UNEXTENDED — worker `mtp_spec_capable` stays
182//! false for glm5_next plans; the serving capability lives in its OWN manifest
183//! (`GLM5_SPEC`, execution_manifest.rs) whose table names exactly the glm5_next class, and
184//! a SEALED production bundle still fails closed until it banks a `glm5-spec.v1` rewrite
185//! receipt (the real-artifact qualification lane's job).
186
187use crate::Engine;
188use crate::cache::{Cache, HcTapSink};
189use crate::dflash::{DflashDraft, DflashKv, DsparkDraftSample};
190use crate::forward::argmax;
191use crate::hybrid::{HybridModel, Mixer};
192use crate::spec::SpecSampling;
193use crate::spec_phase::{
194 ProfClock, SPEC_PROF_ROUNDS, SpecFirstTokenProf, SpecPhaseNs, SpecRoundProf, SpecRoundsLog,
195 V_SEQ_ROWS, spec_prof_on,
196};
197use cudarc::driver::CudaSlice;
198
199type Res<T> = Result<T, Box<dyn std::error::Error>>;
200/// Round-cadence commit hook of `glm5_spec_session_burst_streamed` (lane/b200-spec-ttft-
201/// 20260902): called with every newly committed slice of a burst, in order, disjoint.
202pub type CommitHook<'a> = &'a mut dyn FnMut(&[u32]);
203
204/// `MEMRA_GLM5_SPEC=1` routes `generate_spec` to the glm5 T-parallel loop. Default OFF:
205/// unset/0 keeps the standing `refuse_hyper` refusal, so serving is byte-identical to the
206/// pre-lane binary. Read once (worker chunk policies read their flags the same way).
207pub fn glm5_spec_on() -> bool {
208 use std::sync::OnceLock;
209 static ON: OnceLock<bool> = OnceLock::new();
210 *ON.get_or_init(|| std::env::var("MEMRA_GLM5_SPEC").as_deref() == Ok("1"))
211}
212
213// Per-burst phase attribution moved to `crate::spec_phase` (lane/glm5-extract-general):
214// the draft/verify/accept/roll/maint split is spec-family-generic. This loop consumes
215// `MEMRA_SPEC_TRACE` (glm5 alias `MEMRA_GLM5_SPEC_TRACE` stays honored) and passes its
216// own `[glm5-phase]` / `[glm5-phase-v]` tags so every banked receipt keeps its shape.
217
218/// `MEMRA_GLM5_VERIFY_BATCH` (default ON, lane/glm5-verify-batch): the per-LAYER batched
219/// mixer walk — one t=K+1 KDA call per layer (projections/conv/gates batched through the
220/// decode-exact classes, the recurrence sequential INSIDE one scan launch) and one
221/// t=K+1 rows-exact MLA call per layer, replacing the per-row mixer loop. `0` restores
222/// the per-row walk byte-for-byte — the rollback seam. Deliberate default (new-flags
223/// law): the walk only exists behind `MEMRA_GLM5_SPEC` (default OFF in prod), per-row
224/// byte identity is bit-gated on the rig (`glm5_tparallel_verify_gpu` +
225/// `glm5_verify_batch_gpu`), and the box re-battery A/Bs this seam in one build. Read
226/// PER CALL (the `MEMRA_KDA_FUSED_PROJ` per-call precedent) so gates drive both arms in
227/// one process; one env read per verify walk.
228pub fn glm5_verify_batch_on() -> bool {
229 std::env::var("MEMRA_GLM5_VERIFY_BATCH").as_deref() != Ok("0")
230}
231
232/// `MEMRA_GLM5_DRAFT_TAPS_DEVICE` (DEFAULT OFF; `=1` arms it; lane/spec-route-depth-20260902):
233/// the DEVICE-RESIDENT drafter
234/// prime. Boot D on the 2x B200 pair (this arm) against boot C (host taps), TTFT s at
235/// 4k / 42k / 128k / 256k: 4.89 / 16.84 / 56.62 / 129.42 vs 5.32 / 21.23 / 72.90 /
236/// 162.26, drafter prime ms 17.9 / 59.1 / 180.7 / 362.4 vs 91.4 / 561.6 / 4430.8 /
237/// 8868.4, steady rounds and decode-after unchanged (FLAGS row, receipt path there). The trunk prime stays the ONE whole-prompt call (the
238/// chunked arm's per-range calls re-entered the PP-2 microchunk geometry and made the trunk
239/// prime itself far slower — boot B); the tap rows never leave the device: the walk stages
240/// each range's five contracted tap planes on the writing stage's device (a chunk-sized
241/// ring, `HcTapSink::new_device_staged_at`), and at the range boundary the prime's own loop
242/// hands the range to `glm5_taps_range_done`, which interleaves the planes into the fc
243/// layout with 2D device copies (a peer copy first for planes written on another stage),
244/// runs `ctx_features` + `ingest_ctx` at the range width (4096 rows: the batched GEMM
245/// class), and appends to the drafter KV. No DtoH in the prime, no HtoD in the drafter
246/// prime; the eager arm's 7.5 s of pageable HtoD at 256k (boot B's split: h2d 7505.8 of
247/// 8870 ms) goes away outright, and its `tap_dtoh` with it. Read per session creation
248/// (a gate flips it in-process). The host-tap arm (`MEMRA_GLM5_DRAFT_PRIME_LAZY`) is the
249/// default and is reachable whenever this is unset.
250///
251/// DEFAULT OFF DELIBERATELY, and the flip is blocked on two named defects that a review of
252/// the ON default found. Both are properties of the device-staged ring, so neither can bite
253/// while this is unset. FIRST, the `MEMRA_B200_PRIME_V2` arm-2 branch
254/// (`prime_cache_hyper_pp2_pipelined`) never calls
255/// `glm5_taps_range_begin`/`glm5_taps_range_done`, and hands each stage-chunk an ABSOLUTE
256/// base, so from the second range on `glm5_hc_tap_slot` slices past the `sink.t * h` slot
257/// buffer: `slice_mut` panics and takes the worker with it. SECOND, the same-`ordinal`
258/// ingest reads the ring `buf` from the head engine's stream while the next range rewrites
259/// it on the stage engine's stream, with cudarc implicit event tracking disabled in this
260/// repo, so the tap planes can tear into silently wrong drafter ctx KV; verify still
261/// arbitrates the served tape, so the tape-identity gates stay green and would NOT catch
262/// it. Flipping the default needs both fixed AND a gate that fails on each before it
263/// passes.
264pub fn glm5_draft_taps_device_on() -> bool {
265 std::env::var("MEMRA_GLM5_DRAFT_TAPS_DEVICE").as_deref() == Ok("1")
266}
267
268/// The device-resident drafter prime's in-flight state (doc on
269/// [`glm5_draft_taps_device_on`]), carried type-erased on the prime's tap sink
270/// (`HcTapSink::ingest_state`) so the prime's range loop can hand it each range.
271struct Glm5DraftPrimeInflight {
272 kv: DflashKv,
273 taps: Vec<usize>,
274 n_embd: usize,
275 /// Rows the sink ring covers (the largest range the schedule emits).
276 ring: usize,
277 /// Head-device staging for planes written on another stage (lazily sized `ring x h`).
278 stage: Vec<Option<CudaSlice<f32>>>,
279 /// The interleaved `[ring, n_taps * h]` fc input on the head device (lazy).
280 rows_dev: Option<CudaSlice<f32>>,
281 prof_on: bool,
282 copy_ms: f64,
283 feat_ms: f64,
284 kv_ms: f64,
285 chunks: usize,
286}
287
288/// `MEMRA_GLM5_DRAFT_PRIME_LAZY` (default OFF, lane/spec-route-depth-20260902): `=1`
289/// restores the pre-lane placement of the EAGER arm's drafter ingest — inside round 1 of
290/// the first burst, i.e. AFTER the prime's anchor token has been emitted under the
291/// round-cadence door. Default (unset): the ingest runs at session creation, before the
292/// session is returned and before any token is emitted. WHY THE FLIP: the 2x B200 pair's
293/// boot A (`MEMRA_SPEC_PROF=1`, main + PR #101) measured the round-0 wall at 0.63 / 4.5 /
294/// 8.9 s for 42k / 128k / 256k prompts (about 35 us per prompt token, the eager ingest)
295/// while every later round sits at 55-64 ms; with the anchor already streamed, that one
296/// round lands INSIDE decode, which is the bimodal "15.4 vs 32.1 tok/s" the pair saw
297/// (256 tokens over 8.9 s + 255 rounds/37 tok/s = 16 tok/s; without the stall, 37). The
298/// work is identical either way (same rows, same GEMMs, same KV bytes: the KV is
299/// position-addressed and nothing touches it between creation and round 1); only WHEN it
300/// runs moves, from the second token's latency to TTFT. Read per session creation.
301pub fn glm5_draft_prime_lazy_on() -> bool {
302 std::env::var("MEMRA_GLM5_DRAFT_PRIME_LAZY").as_deref() == Ok("1")
303}
304
305/// Drafter ctx KV bytes at `cap` rows (the `DflashKv::new` geometry), for the profile.
306fn dflash_kv_bytes(cfg: &crate::dflash::DflashCfg, cap: usize) -> usize {
307 2 * cfg.n_layer * (cap + cfg.block_size) * cfg.n_kv * cfg.head_dim * std::mem::size_of::<f32>()
308}
309
310/// `MEMRA_GLM5_SPEC_PREFIX` (default OFF, lane/glm5-prefix-latent2 2026-09-01): the glm5
311/// spec x prefix-cache interplay — BOTH sides, one flag (the `MEMRA_DSPARK_PREFIX_RESTORE`
312/// precedent): (capture) DFlash2-source sessions take a prompt-boundary capture at creation
313/// for the worker's deferred prefix publication, and (restore) the worker may convert a
314/// prefix hit into `glm5_spec_session_from_restored` instead of demoting it to the plain
315/// route. Requires `MEMRA_PREFIX_LATENT=1` too — a capture the worker's publisher would
316/// refuse (latent entries need the plane flag) is pure waste, so this predicate ANDs both
317/// env reads. DEFAULT OFF BY DESIGN (new-flags law): the restored-session program is
318/// unmeasured until the box battery banks restored-vs-cold byte identity on the
319/// continuation; unset restores the pre-lane posture exactly (spec sessions never capture,
320/// hits demote to plain). Read once per process.
321pub fn glm5_spec_prefix_on() -> bool {
322 use std::sync::OnceLock;
323 static ON: OnceLock<bool> = OnceLock::new();
324 *ON.get_or_init(|| {
325 let sp = std::env::var("MEMRA_GLM5_SPEC_PREFIX").as_deref() == Ok("1");
326 let pl = std::env::var("MEMRA_PREFIX_LATENT").as_deref() == Ok("1");
327 if sp && !pl {
328 // A mis-built recipe would otherwise only surface through the battery's
329 // receipt gates (PR #96 review round 2, minor) — say it at first read.
330 eprintln!(
331 "[glm5-spec] MEMRA_GLM5_SPEC_PREFIX=1 is INERT: it requires \
332 MEMRA_PREFIX_LATENT=1 (latent entries could never publish without it)"
333 );
334 }
335 sp && pl
336 })
337}
338
339/// `MEMRA_GLM5_SPEC_FULLCOVER` (default OFF, lane/glm5-fullcover-spec-route 2026-09-02):
340/// admit the glm5 spec route on a FULL-COVER prefix hit, i.e. a hit whose restored prefix
341/// already covers the whole prompt so there is no suffix left to prime.
342///
343/// WHY IT EXISTS (memra#74): the parent lane refused an empty suffix on the premise that
344/// "the plain boundary-logits resume is faster than any prime". That is true of PREFILL and
345/// says nothing about DECODE: with the drafter left un-armed the whole generation then ran
346/// at plain speed. Measured on the live glm5 box 2026-09-02, same minute and vantage,
347/// vendor-default sampled, 67-token prompt, 512 max_tokens: repeated (full-cover hit)
348/// 29.46 s wall median / 30.8 tok/s decode vs fresh-nonce (cold, route=spec) 57.75 / 69.7.
349/// A cache hit cost the customer half the decode speed on that shape.
350///
351/// WHY IT IS WELL-FORMED: with an empty suffix the restored session's state is exactly a
352/// cold session's at the same boundary: trunk cache at `fed.len()`, drafter ctx KV at
353/// `fed.len()` (`DflashKv::from_tail`, the same rows a cold prime's tap ingest would have
354/// produced), no pending tap rows, and the anchor drawn from the ENTRY's boundary logits by
355/// the same rule the cold burst applies to its own first token. The Dflash2 round invariant
356/// `kv.len == cache.pos` therefore holds at round 1 with `pending` empty. This is the same
357/// full-cover shape the MTP restore has served since lane/spec-cache
358/// (`spec_restore_refusal`: a full-cover hit is admitted whenever the entry carries its
359/// boundary hidden + logits).
360///
361/// DEFAULT OFF BY DESIGN (new-flags law): the arm has NO GPU receipt yet. Byte identity of
362/// the full-cover restored tape against plain decode is GATE 13 of
363/// `glm5_dflash_session_gpu` (`MEMRA_GLM5_SPEC_FULLCOVER=1`), and the serving win needs the
364/// repeated-prompt cell on the glm5 box. Unset restores the pre-lane posture exactly
365/// (full-cover hits serve plain, now with `reason=full-cover-hit` on the route line).
366/// Read PER CALL (the `MEMRA_KDA_FUSED_PROJ` / `MEMRA_GLM5_VERIFY_BATCH` precedent) so one
367/// gate process can drive both arms. Rollback seam: unset.
368pub fn glm5_spec_fullcover_on() -> bool {
369 std::env::var("MEMRA_GLM5_SPEC_FULLCOVER").as_deref() == Ok("1")
370}
371
372/// `MEMRA_GLM5_SPEC_TP` (default OFF, lane/glm5-composition 2026-09-01): admit glm5 spec
373/// SESSIONS on a `MEMRA_GLM5_TP`-armed model. DEFAULT OFF BY DESIGN (new-flags law): the
374/// composition's verify/rollback wiring is rig-gated for correctness (per-rank KDA
375/// snapshot/replay, per-replica MLA latent truncation — `glm5-tp-gate` arms S*), but it has
376/// ZERO real-artifact receipts and the TP serving wiring is still the named box increment;
377/// an unmeasured composition does not default ON. `=1` lifts ONLY the session co-refusal —
378/// every other admission law holds (draft source required, batched verify walk required:
379/// the per-row rollback seam carries no TP arm and refuses by name). Read per session
380/// creation. Rollback seam: unset (the co-refusal is restored verbatim).
381pub fn glm5_spec_tp_on() -> bool {
382 std::env::var("MEMRA_GLM5_SPEC_TP").as_deref() == Ok("1")
383}
384
385/// `MEMRA_SPEC_PENALTY` (default OFF, lane/spec-exclusions-20260902): admit PENALIZED
386/// requests (repetition / frequency / presence, greedy AND sampled) to the glm5 spec route.
387/// Pre-lane every penalized request served plain (`reason=penalized`), which on agent
388/// harnesses that ship a vendor-default `presence_penalty` was the whole route's 1.48x lost
389/// on the most common request shape there is. THE ARM: every verify round penalizes a COPY of
390/// the target's verify rows on device over the per-row evolving window (`penalize_logits_rows_inc`
391/// — row r sees the session window ++ the drafts before r, exactly the plain sampler's history
392/// at that position), and the accept reads only that copy: greedy argmaxes it, the sampled
393/// walk takes its p from it, the bonus and the residual draw from it; the anchor is drawn the
394/// same way from the prime's boundary row. The DRAFT stays unpenalized (it only proposes;
395/// rejection sampling is unbiased for any proposal). NUMERIC CLASS: the device pass is the
396/// host sampler's arithmetic bit for bit (`keskar_penalize_rn`, gated by
397/// `gpu_device_penalties_are_bit_identical_to_the_host_sampler`), so greedy+penalties keeps
398/// the spec-vs-plain tape identity (gate 15) and sampled+penalties keeps the route's
399/// distribution-exact claim: the accepted stream equals the plain penalized sampler's
400/// distribution. DEFAULT OFF BY DESIGN (new-flags law): the arm has rig receipts only; the
401/// box battery (sampled vendor-default + penalties, 8-turn twin) flips it. Unset = the
402/// pre-lane refusal verbatim (the worker keeps penalized requests plain, by name). Read per
403/// session creation (the `MEMRA_GLM5_SPEC_FULLCOVER` precedent) so one gate process drives
404/// both arms.
405pub fn glm5_spec_penalty_on() -> bool {
406 std::env::var("MEMRA_SPEC_PENALTY").as_deref() == Ok("1")
407}
408
409/// `MEMRA_SPEC_WARM` (default OFF, lane/spec-exclusions-20260902): admit a prefix-hit
410/// CARRIER whose entry carries NO usable drafter tail (`reason=no-drafter-tail`: the entry
411/// was published by a plain session — a K-shed turn, a constrained or vision turn — or its
412/// tail did not cover the drafter window) by re-arming the drafter COLD at the restored
413/// boundary (`DflashKv::new_cold_at`): the trunk restores, the drafter starts with an empty
414/// context at the right absolute position and fills it from the suffix prime's taps and every
415/// committed round. Rows below the floor are never attended (the clipped round attention's
416/// floor arm), so the drafter sees exactly the program a shorter prompt runs. Can only move
417/// ACCEPTANCE, never output (verify arbitrates; gate 18 pins the restored tape against plain
418/// decode). Lives UNDER `MEMRA_GLM5_SPEC_PREFIX` (x `MEMRA_PREFIX_LATENT`): without the restore
419/// door there is no carrier to warm. DEFAULT OFF BY DESIGN: the acceptance cost of the empty
420/// context on the first rounds is unmeasured on the real artifact. Read per request.
421pub fn glm5_spec_warm_on() -> bool {
422 std::env::var("MEMRA_SPEC_WARM").as_deref() == Ok("1")
423}
424
425/// The penalty arm's per-session config (doc on [`glm5_spec_penalty_on`]): the request's
426/// Keskar coefficients and window, INDEPENDENT of the sampling regime — a greedy request
427/// with penalties admits too, which is why this is not folded into `Option<SpecSampling>`
428/// (whose `None` means greedy everywhere else on the route).
429#[derive(Clone, Copy, Debug)]
430pub struct Glm5Penalty {
431 last_n: usize,
432 rep: f32,
433 freq: f32,
434 present: f32,
435}
436
437impl Glm5Penalty {
438 /// `Some` iff the request carries a non-identity penalty with an armed window — THE
439 /// `pen_on` predicate (one definition, `SpecSampling::pen_on`).
440 pub fn of(sp: &SpecSampling) -> Option<Self> {
441 sp.pen_on().then_some(Self {
442 last_n: sp.penalty_last_n,
443 rep: sp.penalty_repeat,
444 freq: sp.penalty_freq,
445 present: sp.penalty_present,
446 })
447 }
448 /// The device window: `penalty_last_n` under the `PEN_WINDOW_MAX` cost bound — the same
449 /// trim the dspark accept walk and the boundary draw apply.
450 fn win(&self) -> usize {
451 self.last_n.min(crate::spec::PEN_WINDOW_MAX)
452 }
453}
454
455/// Print-once latch for the penalty arm's engagement line (one per process).
456static PENALTY_ARM_ANNOUNCED: std::sync::atomic::AtomicBool =
457 std::sync::atomic::AtomicBool::new(false);
458
459/// The penalty admission law, shared by the cold and restored session constructors: a
460/// penalized request opens a session ONLY under `MEMRA_SPEC_PENALTY=1`; otherwise it refuses
461/// loudly (worker admission owns the exclusion and keeps such requests plain — silently
462/// dropping a request's penalties is the failure class the refusal prevents). Logs the
463/// engagement receipt once per process on the first penalized session that opens.
464fn glm5_penalty_admit(sampling: Option<&SpecSampling>) -> Res<Option<Glm5Penalty>> {
465 let Some(pen) = sampling.and_then(Glm5Penalty::of) else {
466 return Ok(None);
467 };
468 if !glm5_spec_penalty_on() {
469 return Err(
470 "glm5 spec penalty arm is DARK (MEMRA_SPEC_PENALTY unset): penalized requests serve \
471 on the plain path (worker admission owns the exclusion; silently dropping the \
472 request's penalties is the failure class this refusal prevents)"
473 .into(),
474 );
475 }
476 if !PENALTY_ARM_ANNOUNCED.swap(true, std::sync::atomic::Ordering::AcqRel) {
477 eprintln!(
478 "[glm5-spec] penalty arm ENGAGED (MEMRA_SPEC_PENALTY=1): verify rows penalized on \
479 device over the session window (rep={} freq={} present={} last_n={}); printed \
480 once per process, every penalized request carries penalized=1 on its route line",
481 pen.rep, pen.freq, pen.present, pen.last_n,
482 );
483 }
484 Ok(Some(pen))
485}
486
487/// Seed the session's penalty window from its committed prompt (`spec::pen_window_seed`, one
488/// definition of "the window" across every spec route). Empty with penalties off.
489fn glm5_pen_window_seed(pen: Option<&Glm5Penalty>, committed: &[u32]) -> Vec<u32> {
490 match pen {
491 Some(p) => crate::spec::pen_window_seed(&[], committed, p.last_n),
492 None => Vec::new(),
493 }
494}
495
496/// The session's FIRST token from a host boundary-logits row (the prime's, or the entry's on
497/// a full-cover restore) — the plain route's own first-token rule, regime by regime:
498/// * sampled: `sample_boundary_token` (penalize over the window, filter, Gumbel at the
499/// session's Philox counter) — with the window it was always handed empty before;
500/// * greedy + penalties: the plain sampler's `sample` is penalize-then-argmax, so the row
501/// goes to the device, takes the one penalty pass (`penalize_logits`, the same bits the
502/// host produces), and the device argmax (host tie-break contract) picks;
503/// * greedy: the host argmax, byte for byte the pre-lane literal.
504#[allow(clippy::too_many_arguments)]
505// allow: the three regimes' inputs, listed rather than bundled, so the OFF arm reads as the
506// pre-lane literal at the call site
507fn glm5_anchor(
508 eh: &Engine,
509 logits: &[f32],
510 sampling: Option<&SpecSampling>,
511 pen: Option<&Glm5Penalty>,
512 pen_hist: &[u32],
513 sctr: &mut u32,
514 site: &str,
515) -> Res<u32> {
516 match (sampling, pen) {
517 (Some(sp), _) => crate::spec::sample_boundary_token(eh, logits, sp, pen_hist, sctr, site),
518 (None, Some(p)) => {
519 let n = logits.len();
520 let mut col = eh.htod(logits)?;
521 let w0 = pen_hist.len().saturating_sub(p.win());
522 let hist = &pen_hist[w0..];
523 if !hist.is_empty() {
524 let hd = eh.htod_u32_v(hist)?;
525 eh.penalize_logits(&mut col, &hd, hist.len(), p.rep, p.freq, p.present, n)?;
526 }
527 let td = eh.argmax_token_device(&col, n)?;
528 crate::spec::guard_vocab_token(
529 eh.dtoh_u32_one(&td)?,
530 n,
531 &format!("glm5 penalized greedy anchor ({site})"),
532 )
533 }
534 (None, None) => Ok(argmax(logits) as u32),
535 }
536}
537
538/// `MEMRA_SPEC_PMIN`, honored by the glm5 loop (loop-port 2 — the step37 shipping family,
539/// `MEMRA_SPEC_PMIN=0.5 MEMRA_SPEC_PMIN0=1` is what step37 serves; NO new flag): stop the
540/// draft chain early when the drafter's confidence in its own pick drops below p_min.
541/// Native chain: p = the head's softmax confidence in its pick (the spec.rs `g_p`
542/// statistic, `prob_of_token_device`). DFlash2: q = the selector's recorded per-slot
543/// candidate-set confidence (`q_chosen`; T=1 twin on the greedy walk) — the owner's
544/// "take only high confidence offers" tau-slot form, truncated PRE-verify.
545/// Unset/0 = OFF (today's rounds, byte-identical). The VALUE is a per-model measurement
546/// (spec.rs bank: q27 PMIN=0.3 was -1.9% on one pack; step37 ships 0.5) — the box-B tau
547/// ladder prices glm5's.
548pub(crate) fn glm5_pmin() -> f32 {
549 use std::sync::OnceLock;
550 static P: OnceLock<f32> = OnceLock::new();
551 *P.get_or_init(|| {
552 std::env::var("MEMRA_SPEC_PMIN")
553 .ok()
554 .and_then(|v| v.parse().ok())
555 .unwrap_or(0.0)
556 })
557}
558
559/// `MEMRA_SPEC_PMIN0=1` (llama.cpp's draft gating, vendored via spec.rs): the p-min gate
560/// applies at slot 0 too, so a low-confidence round drafts NOTHING and the verify batch is
561/// just the anchor row — m=1 = a plain decode step. Always legal for glm5 (the anchor row
562/// exists every round). "llama's 35B win rides exactly this — draft acceptance 76% at mean
563/// len 2.5 because unpredictable stretches never pay draft+verify overhead" (spec.rs).
564pub(crate) fn glm5_pmin0() -> bool {
565 use std::sync::OnceLock;
566 static P: OnceLock<bool> = OnceLock::new();
567 *P.get_or_init(|| std::env::var("MEMRA_SPEC_PMIN0").as_deref() == Ok("1"))
568}
569
570/// MEMRA_SPEC_PMIN break semantics — hoisted to the shared K-policy surface
571/// ([`crate::spec::spec_conf_keep`], lane/glm5-extract-general); re-exported here so the
572/// glm5 gates and call sites keep their name.
573pub use crate::spec::spec_conf_keep as glm5_conf_keep;
574
575/// The loaded DFlash2 drafter (module doc, DRAFT SOURCE SEAM): the model-level half of the
576/// `Dflash2` draft source — weights loaded ONCE per model on the head engine (`hybrid.rs`,
577/// `MEMRA_GLM5_DFLASH`); per-session state lives in [`Glm5DraftState`].
578///
579/// HOISTED to the general seam ([`crate::dflash::DflashDrafter`], lane/glm5-extract2): the
580/// holder is `{ drafter weights, byte-identity pin }` with nothing glm5 in it. Re-exported
581/// here under its old name so glm5's call sites and gates keep the name they were written
582/// against, exactly as `glm5_conf_keep` does above.
583pub use crate::dflash::DflashDrafter as Glm5DflashDrafter;
584
585/// Process-wide count of verify rounds whose drafts came through a RANK-TRIMMED draft head
586/// (either source; lane/frspec-dflash2-20260902). Gates read it beside the per-session
587/// [`Glm5SpecSession::rank_trimmed_rounds`]; the worker's `[glm5-acc]` line carries the
588/// session's own count.
589static GLM5_RANK_TRIMMED_DRAFT_ROUNDS: std::sync::atomic::AtomicU64 =
590 std::sync::atomic::AtomicU64::new(0);
591
592/// Read [`GLM5_RANK_TRIMMED_DRAFT_ROUNDS`] (doc on the static).
593pub fn glm5_rank_trimmed_draft_rounds() -> u64 {
594 GLM5_RANK_TRIMMED_DRAFT_ROUNDS.load(std::sync::atomic::Ordering::Relaxed)
595}
596
597/// Per-session draft-source state (module doc, DRAFT SOURCE SEAM). Selected at session
598/// creation from the model's loaded sources and pinned for the session's lifetime.
599pub(crate) enum Glm5DraftState {
600 /// Embedded NextN head: state = the MTP latent plane + `Glm5SpecSession::pending`
601 /// (token, h_seed) pairs — the pre-seam program, byte-identical.
602 NativeMtp,
603 /// DFlash2 block-diffusion drafter: state = the drafter's own ctx-feature KV cache
604 /// plus host feature rows not yet ingested. Invariant at every round boundary:
605 /// `kv.len + pending.len()/(taps.len()*n_embd) == committed.len()` — the drafter's
606 /// context is exactly the committed tokens (the probe's `F_feat[new_lo:start]` walk).
607 Dflash2 {
608 kv: DflashKv,
609 /// Committed-position feature rows awaiting ingest, `[n, n_taps*n_embd]` host
610 /// (the prompt's prime taps at session start; each round's kept verify taps after).
611 pending: Vec<f32>,
612 /// Resolved tap layers (drafter config `target_layer_ids`, red-arm shift applied).
613 taps: Vec<usize>,
614 },
615}
616
617/// The retained q side of one round's draft chain — what the sampled accept walk consumes.
618/// Greedy rounds carry `None` (the accept is the byte-deterministic prefix walk).
619enum Glm5DraftQ {
620 None,
621 /// Native MTP chain: per-slot retained draft logits (rank space under a trim) + the
622 /// filtered stats of the distribution each draft was drawn from.
623 Mtp {
624 draft_idx: Vec<u32>,
625 draft_logits: Vec<CudaSlice<f32>>,
626 draft_stats: Vec<(f32, f32, f32)>,
627 },
628 /// DFlash2 selector proposal (the recorded candidate-set q) + the retained draft-logit
629 /// rows `dl` (`dspark_accept_sampled`'s buffer contract; unread on the Selector q path).
630 Selector {
631 prop: DsparkDraftSample,
632 dl: CudaSlice<f32>,
633 },
634}
635
636/// Resolve the drafter's tap layers against the trunk: the drafter config's
637/// `target_layer_ids` are memra PLAN layer indices whose COMPLETED output feeds the fc
638/// (the probe's capture convention: `MEMRA_TRACE_LAYER_ROWS_LAYERS=5,14,24,33,42` == the
639/// drafter's own `target_layer_ids`, asserted 1:1 in `score_dflash2.py`).
640/// `MEMRA_GLM5_DFLASH_GATE_RED=tap-shift` is the RED-ARM INSTRUMENT: every tap moves +1
641/// layer — deliberately wrong features whose acceptance collapse the gate asserts while
642/// the output tape stays byte-identical. Unknown values refuse loudly.
643///
644/// The resolution itself is the general seam ([`crate::dflash::resolve_tap_layers`],
645/// lane/glm5-extract2); what stays here is glm5's OWN red arm — the gate instrument reads its
646/// env, prints its `[glm5-spec]` tag, and hands the shift in as a parameter. Error bytes are
647/// unchanged ("glm5 DFlash2" is the `what` label).
648fn glm5_dflash_tap_layers(draft: &DflashDraft, n_trunk: usize) -> Res<Vec<usize>> {
649 let shift = match std::env::var("MEMRA_GLM5_DFLASH_GATE_RED").ok().as_deref() {
650 Some("tap-shift") => {
651 eprintln!(
652 "[glm5-spec] RED-ARM tap-shift: drafter tap layers shifted +1 (gate \
653 instrument, never a serving flag)"
654 );
655 1
656 }
657 Some("") | None => 0,
658 Some(other) => {
659 return Err(format!(
660 "MEMRA_GLM5_DFLASH_GATE_RED={other:?}: unknown red arm (want tap-shift)"
661 )
662 .into());
663 }
664 };
665 Ok(crate::dflash::resolve_tap_layers(
666 &draft.cfg.target_layer_ids,
667 n_trunk,
668 shift,
669 "glm5 DFlash2",
670 )?)
671}
672
673/// Pre-round state checkpoint for one glm5 verify round. Captured by `glm5_verify_rows`
674/// BEFORE any row runs; consumed by `glm5_verify_rollback`.
675///
676/// Covers exactly the state planes a glm5_next trunk mutates in a verify round:
677/// - `latent_len`: per-layer MLA latent length at round start (rollback = truncate).
678/// - KDA state (loop-port 3, the module doc's GdnStash/ReplaySSM diet LANDED): the old
679/// per-row (conv, ssm) column clones — 4 MiB x 34 layers per COLUMN, ~0.95 GiB of
680/// transient at K=7 — are replaced by
681/// * `kda_ssm_snap[il]`: ONE recurrent-state clone per layer per round (the state
682/// BEFORE row 0),
683/// * `kda_scan_stash[il][r]`: row r's scan-input buffers, STOLEN from the step (zero
684/// copies, ~160 KB/row/layer — `kda::KdaScanInputs`), rows `0..t-1` except the last
685/// (`keep == t` needs no restore, so row `t-1` is never a replay target),
686/// * `kda_conv_cols[il][r]`: the conv ring stays PER-ROW CLONED (288 KiB, 1.4% of the
687/// ssm plane it rode beside — not worth a replay arm).
688///
689/// Partial-accept rollback REPLAYS rows `0..keep` from the snapshot
690/// (`kda::kda_scan_replay`): each replay is the original t=1 scan launch re-issued over
691/// the very buffers that row consumed, so the rebuilt state is byte-identical to the
692/// clone it replaces by construction. Under a ppN split every clone/stash lives on its
693/// layer's OWNING stage engine; rollback restores through the same per-stage seam.
694/// - `pos`: `cache.pos` at round start.
695///
696/// glm5_next has no Full/Linear trunk mixers (the walk refuses them by name), so `kv`,
697/// `tp_kv` and GDN stashes have no arm here — growing one is a deliberate extension with
698/// its own gate, not a silent default.
699pub struct Glm5VerifyCkpt {
700 pos: usize,
701 latent_len: Vec<Option<usize>>,
702 /// Per-row conv-ring clones, rows `0..t-1` except the last (doc above). PER-ROW walk
703 /// only (`MEMRA_GLM5_VERIFY_BATCH=0`); the batched walk fills `kda_rows` instead.
704 kda_conv_cols: Vec<Option<Vec<CudaSlice<f32>>>>,
705 /// The recurrent state BEFORE row 0, one clone per KDA layer per round (doc above).
706 /// BOTH walks fill this — it is the batched replay's scan base too.
707 kda_ssm_snap: Vec<Option<CudaSlice<f32>>>,
708 /// Stolen per-row scan inputs, rows `0..t-1` except the last (doc above). PER-ROW
709 /// walk only.
710 kda_scan_stash: Vec<Option<Vec<crate::kda::KdaScanInputs>>>,
711 /// BATCHED walk (lane/glm5-verify-batch): one [`crate::kda::KdaRowsStash`] per KDA
712 /// layer per round — ring snapshot + stolen raw conv rows + stolen batched scan
713 /// inputs; rollback re-rolls the ring and replays the scan ONCE at T=keep.
714 kda_rows: Vec<Option<crate::kda::KdaRowsStash>>,
715 /// glm5 TP composition (lane/glm5-composition): per-rank rollback material of each
716 /// SHARDED KDA layer's batched verify call — the rank-indexed twin of
717 /// (`kda_ssm_snap`, `kda_rows`), restored through each rank's own engine. `None` on
718 /// every unsharded layer.
719 kda_tp: Vec<Option<crate::glm5_tp::Glm5TpKdaVerifyStash>>,
720 /// Row count of the walk that filled this ckpt; rollback validates `keep` against it.
721 rows: usize,
722}
723
724impl Glm5VerifyCkpt {
725 /// GATE RECEIPT (wiring anchor, not a serving surface): how many KDA layers filled
726 /// the BATCHED rows stash vs the PER-ROW column stash — the flag A/B gate asserts
727 /// the arm it set actually ran (wiring-assertions-match-prose law: anchor on the
728 /// invocation's artifact, never the log prose).
729 pub fn kda_stash_kinds(&self) -> (usize, usize) {
730 (
731 self.kda_rows.iter().filter(|s| s.is_some()).count(),
732 self.kda_conv_cols.iter().filter(|s| s.is_some()).count(),
733 )
734 }
735}
736
737/// Position buffers for one verify walk range (per stage engine under a split — the
738/// per-stage pos_d law): `all` = the `[t]` vector the BATCHED per-layer mixer calls
739/// consume; `rows` = the per-row single-position buffers of the per-row arm, built only
740/// when that arm can run (flag off) — the batched arm never reads them.
741struct Glm5VerifyPos {
742 pos0: usize,
743 t: usize,
744 all: CudaSlice<i32>,
745 rows: Vec<CudaSlice<i32>>,
746}
747
748impl Glm5VerifyPos {
749 fn new(e: &Engine, pos0: usize, t: usize) -> Res<Self> {
750 let v: Vec<i32> = (0..t as i32).map(|r| pos0 as i32 + r).collect();
751 let all = e.htod_i32(&v)?;
752 let rows = if glm5_verify_batch_on() && t > 1 {
753 Vec::new()
754 } else {
755 (0..t)
756 .map(|r| e.htod_i32(&[(pos0 + r) as i32]))
757 .collect::<Result<_, _>>()?
758 };
759 Ok(Self { pos0, t, all, rows })
760 }
761}
762
763impl HybridModel {
764 /// THE T-PARALLEL VERIFY WALK: score `tokens` (row 0 = the last committed token, rows
765 /// 1..t = the K drafted tokens) in ONE forward over the hc trunk at positions
766 /// `cache.pos .. cache.pos + t`, in the batched-decode kernel classes (module doc).
767 ///
768 /// Returns `(logits [t, n_vocab] device, collapsed [t, n_embd] device, ckpt)`:
769 /// `logits` row r is bit-identical to the plain `decode_step_hyper` logits after
770 /// consuming `tokens[r]` at that position (the gate's bar); `collapsed` row r is the
771 /// pre-output_norm hidden — the MTP `h_seed` for position `cache.pos + r`.
772 ///
773 /// State effects: every trunk MLA plane appends `t` rows; every trunk KDA state
774 /// advances `t` steps (per-step columns stashed in the ckpt); `cache.pos` is NOT moved
775 /// (rollback owns it). The MTP block's plane (il = n_trunk) is untouched.
776 pub fn glm5_verify_rows(
777 &self,
778 e: &Engine,
779 tokens: &[u32],
780 cache: &mut Cache,
781 ) -> Res<(CudaSlice<f32>, CudaSlice<f32>, Glm5VerifyCkpt)> {
782 let topology = *self
783 .hyper
784 .as_ref()
785 .ok_or("glm5_verify_rows on a model with no HyperConnections topology")?;
786 let t = tokens.len();
787 let cap = Self::hyper_batch_cap();
788 if t == 0 {
789 return Err("glm5_verify_rows: empty verify row set".into());
790 }
791 if t > cap {
792 return Err(format!(
793 "glm5_verify_rows: t={t} > cap {cap} — at t >= PRIME_MIN_T (16) the MoE \
794 shared-expert trio crosses off the decode-exact class (the batched-decode \
795 gate's measured B=16 knee), so per-row bit-identity vs plain decode breaks. \
796 K <= cap-1 drafts per round"
797 )
798 .into());
799 }
800 let mut any_sharded = false;
801 for (il, layer) in self.layers.iter().enumerate() {
802 match &layer.mixer {
803 Mixer::Kda(la) => any_sharded |= la.tp.is_some(),
804 Mixer::Mla(mla) => any_sharded |= mla.tp.is_some(),
805 _ => {
806 return Err(format!(
807 "glm5_verify_rows: trunk layer {il} is not a KDA or MLA mixer — the \
808 rollback contract below is built and gated for glm5_next's two state \
809 classes only; a Full/Linear arm needs its own ckpt plane and gate"
810 )
811 .into());
812 }
813 }
814 }
815 // spec x TP composition (lane/glm5-composition): the per-row walk carries no TP
816 // rollback arm — a sharded trunk demands the BATCHED walk at t > 1 (t = 1 rounds
817 // ride the TP decode walk below; full accept is the only legal outcome there).
818 if any_sharded && t > 1 && !glm5_verify_batch_on() {
819 return Err(
820 "glm5_verify_rows: the trunk is glm5-TP-SHARDED and MEMRA_GLM5_VERIFY_BATCH=0 — \
821 the per-row rollback seam carries no TP arm; the spec x TP composition \
822 requires the batched verify walk (unset MEMRA_GLM5_VERIFY_BATCH or run \
823 without the TP door)"
824 .into(),
825 );
826 }
827
828 let n_embd = self.cfg.n_embd as usize;
829 let pos0 = cache.pos;
830
831 // Ckpt BEFORE any state moves.
832 let mut ckpt = Glm5VerifyCkpt {
833 pos: pos0,
834 latent_len: cache
835 .latent
836 .iter()
837 .take(self.layers.len())
838 .map(|plane| plane.as_ref().map(|plane| plane.len))
839 .collect(),
840 kda_conv_cols: (0..self.layers.len()).map(|_| None).collect(),
841 kda_ssm_snap: (0..self.layers.len()).map(|_| None).collect(),
842 kda_scan_stash: (0..self.layers.len()).map(|_| None).collect(),
843 kda_rows: (0..self.layers.len()).map(|_| None).collect(),
844 kda_tp: (0..self.layers.len()).map(|_| None).collect(),
845 rows: t,
846 };
847
848 // ppN door — the verify walk owns its stage split exactly as the batched decode
849 // walk does (`decode_step_batch_hyper_ppn`, decode_batch.rs). Loud refusal on an
850 // unqualified pipeline rewrite, never a single-engine walk over stage-sharded
851 // weights.
852 if let Some(fence) = crate::pp::pp_cuts(self.layers.len()) {
853 if !self.rewrite_allowed(memra_gguf::execution_manifest::RewriteSurface::Pipeline) {
854 return Err("pipeline rewrite is not qualified for this ModelPlan".into());
855 }
856 return self.glm5_verify_rows_ppn(e, tokens, cache, ckpt, &topology, &fence);
857 }
858
859 let pos = Glm5VerifyPos::new(e, pos0, t)?;
860 let embedded = e.htod(&self.embd.try_gather(n_embd, tokens)?)?;
861 let x = crate::hyper::expand(e, &topology, &embedded, t, n_embd)?;
862 let x = self.glm5_verify_range(
863 e,
864 &topology,
865 x,
866 0,
867 self.layers.len(),
868 &pos,
869 cache,
870 &mut ckpt,
871 )?;
872 let (logits, collapsed) = self.glm5_verify_head(e, &topology, &x, t)?;
873 Ok((logits, collapsed, ckpt))
874 }
875
876 /// One hc layer RANGE `[lo, hi)` of the verify walk — the body `glm5_verify_rows` ran
877 /// inline before the ppN twin landed, extracted so the unsplit walk and every pipeline
878 /// stage run the SAME code over their own range (the `hyper_range_decode` /
879 /// `decode_batch_layers` precedent: bit-identity between the arms is then structural,
880 /// not a coincidence of two maintained copies). At `lo=0, hi=n_layers` the launch
881 /// sequence is identical to the pre-extraction walk.
882 ///
883 /// KDA ckpt columns are cloned THROUGH `e` — under a split that is the owning stage's
884 /// engine, so each column lives on the device (and is ordered on the stream) that owns
885 /// its layer's state; `glm5_verify_rollback` restores through the same per-stage seam.
886 #[allow(clippy::too_many_arguments)]
887 // allow: the parameter list mirrors the range-walk call contract its siblings share
888 fn glm5_verify_range(
889 &self,
890 e: &Engine,
891 topology: &crate::hyper::HyperTopology,
892 mut x: CudaSlice<f32>,
893 lo: usize,
894 hi: usize,
895 pos: &Glm5VerifyPos,
896 cache: &mut Cache,
897 ckpt: &mut Glm5VerifyCkpt,
898 ) -> Res<CudaSlice<f32>> {
899 let t = pos.t;
900 let n_embd = self.cfg.n_embd as usize;
901 let eps = self.cfg.rms_eps;
902 // THE BATCHED MIXER ARM (lane/glm5-verify-batch, default ON): one t=K+1 call per
903 // layer per class instead of the per-row loop — KDA batches projections/conv/
904 // gates through the decode-exact classes with the recurrence sequential INSIDE
905 // one scan launch; MLA runs the SAME cached core at t rows on the rows-exact
906 // matmul classes (per-query causal selection + attention by construction).
907 // `0` = the per-row walk below, byte-for-byte (the rollback seam). Engagement is
908 // a receipt, announced once per process.
909 let batch = glm5_verify_batch_on() && t > 1;
910 {
911 static SAID: std::sync::Once = std::sync::Once::new();
912 SAID.call_once(|| {
913 if batch {
914 eprintln!(
915 "[glm5-spec] verify walk BATCHED per layer: kda=one t-call (scan \
916 sequential in-kernel), mla=rows-exact t-call, head=rows-exact, \
917 moe=pairs rows-call where qualified \
918 (MEMRA_GLM5_VERIFY_BATCH default ON)"
919 );
920 } else {
921 eprintln!("[glm5-spec] verify walk PER-ROW (MEMRA_GLM5_VERIFY_BATCH=0 or t=1)");
922 }
923 });
924 }
925 let trace_v = crate::spec_phase::spec_trace_level() >= 2;
926 // Sub-phase clock (trace level 2 only): drain the walking stream so the elapsed
927 // ns lands in the mixer-class bucket — shares, never walls.
928 let vclock = |on: bool| -> Option<std::time::Instant> {
929 on.then(|| {
930 let _ = e.stream().synchronize();
931 std::time::Instant::now()
932 })
933 };
934 for il in lo..hi {
935 let layer = &self.layers[il];
936 let hyper = layer.hyper.as_ref().ok_or_else(|| {
937 format!("layer {il} carries no hyper-connection weights under an hc plan")
938 })?;
939
940 let (y, mix) = crate::hyper::pre_exact(e, topology, &hyper.attn, &x, t, n_embd)?;
941 let mut h = e.uninit(t * n_embd)?;
942 e.rms_norm(&y, layer.attn_norm.float_data(), &mut h, n_embd, t, eps)?;
943 // The batched arm refuses per-layer only for an MLA layer WITHOUT the DSA
944 // indexer: the absorbed t>1 attention arm has no per-row bit-identity claim
945 // at this seam, so it stays on the per-row loop by name (glm5_next always
946 // carries the indexer, so this is a foreign-geometry guard, not a live path).
947 let layer_batched = batch
948 && match &layer.mixer {
949 Mixer::Kda(_) => true,
950 Mixer::Mla(mla) => mla.index.is_some(),
951 Mixer::Full(_) | Mixer::Linear(_) => unreachable!("refused at walk entry"),
952 };
953 let mixed = if layer_batched {
954 match &layer.mixer {
955 // spec x TP composition: sharded mixers ride the TP verify walks —
956 // per-rank batched rows calls, column-parallel-over-gather joins on
957 // the rows-exact classes, per-rank rollback stash into the ckpt.
958 Mixer::Kda(la) if la.tp.is_some() => {
959 let t0 = vclock(trace_v);
960 let mut scan_ns = 0u64;
961 let (out, stash) = crate::glm5_tp::kda_tp_verify_rows(
962 e,
963 la,
964 &h,
965 t,
966 eps,
967 cache,
968 il,
969 trace_v.then_some(&mut scan_ns),
970 )?;
971 ckpt.kda_tp[il] = Some(stash);
972 if let Some(t0) = t0 {
973 let _ = e.stream().synchronize();
974 use std::sync::atomic::Ordering;
975 crate::spec_phase::V_KDA_NS
976 .fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
977 crate::spec_phase::V_KDA_SCAN_NS.fetch_add(scan_ns, Ordering::Relaxed);
978 }
979 out
980 }
981 Mixer::Mla(mla) if mla.tp.is_some() => {
982 let t0 = vclock(trace_v);
983 let out =
984 self.mla_tp_attn_cached(e, mla, &h, &pos.all, t, il, cache, true)?;
985 if let Some(t0) = t0 {
986 let _ = e.stream().synchronize();
987 use std::sync::atomic::Ordering;
988 crate::spec_phase::V_MLA_NS
989 .fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
990 }
991 out
992 }
993 Mixer::Kda(la) => {
994 // Pre-round snapshot: ONE ssm clone per layer per round, BEFORE
995 // the batched call advances the resident state (ckpt doc; also
996 // the batched rollback's scan-replay base).
997 {
998 let rl = cache.recur[il]
999 .as_ref()
1000 .ok_or("glm5 verify KDA layer has no recurrent state")?;
1001 ckpt.kda_ssm_snap[il] = Some(e.clone_dtod(&rl.ssm_state)?);
1002 }
1003 let t0 = vclock(trace_v);
1004 let mut scan_ns = 0u64;
1005 let (out, stash) = crate::kda::kda_verify_rows_cached(
1006 e,
1007 la,
1008 &h,
1009 t,
1010 eps,
1011 cache,
1012 il,
1013 trace_v.then_some(&mut scan_ns),
1014 )?;
1015 ckpt.kda_rows[il] = Some(stash);
1016 if let Some(t0) = t0 {
1017 let _ = e.stream().synchronize();
1018 use std::sync::atomic::Ordering;
1019 crate::spec_phase::V_KDA_NS
1020 .fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1021 crate::spec_phase::V_KDA_SCAN_NS.fetch_add(scan_ns, Ordering::Relaxed);
1022 }
1023 out
1024 }
1025 Mixer::Mla(mla) => {
1026 let t0 = vclock(trace_v);
1027 let out =
1028 self.mla_attn_cached_rows_exact(e, mla, &h, &pos.all, t, il, cache)?;
1029 if let Some(t0) = t0 {
1030 let _ = e.stream().synchronize();
1031 use std::sync::atomic::Ordering;
1032 crate::spec_phase::V_MLA_NS
1033 .fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1034 }
1035 out
1036 }
1037 Mixer::Full(_) | Mixer::Linear(_) => unreachable!("refused at walk entry"),
1038 }
1039 } else {
1040 // ---- PER-ROW mixer walk (the rollback seam; also t=1 rounds and the
1041 // no-indexer MLA guard): row r's state input is row r-1's state output
1042 // (KDA) / rows 0..pos0+r (MLA latent) — each row the SAME t=1 call its
1043 // plain decode step makes.
1044 // h_row is hoisted out of the row loop (loop-port 3): the mixer consumes
1045 // it in stream order before the next row's overwrite, so ONE buffer per
1046 // layer replaces t allocations (stream-ordered pool churn is the dsv4
1047 // lesson).
1048 // Depth attribution (lane/spec-route-depth-20260902): every row that walks
1049 // this arm is counted; the per-round log samples the counter.
1050 V_SEQ_ROWS.fetch_add(t as u64, std::sync::atomic::Ordering::Relaxed);
1051 let mut mixed = e.uninit(t * n_embd)?;
1052 let mut h_row = e.uninit(n_embd)?;
1053 #[allow(clippy::needless_range_loop)]
1054 // allow: r is the sequential row cursor (slices h, offsets pos); iterating pos buffers would hide the row-chaining contract
1055 for r in 0..t {
1056 e.dtod_copy_view(&h.slice(r * n_embd..(r + 1) * n_embd), &mut h_row)?;
1057 // The per-row position buffer: prebuilt when the per-row arm owns the
1058 // walk; built on demand for the rare per-layer refusal under batch.
1059 let pos_row: CudaSlice<i32>;
1060 let pos_r = if let Some(p) = pos.rows.get(r) {
1061 p
1062 } else {
1063 pos_row = e.htod_i32(&[(pos.pos0 + r) as i32])?;
1064 &pos_row
1065 };
1066 let out_row = match &layer.mixer {
1067 // spec x TP composition on the per-row arm. KDA shards reach
1068 // here at t == 1 ONLY, asserted locally: the walk-entry guard is
1069 // a FLAG check two frames up (MEMRA_GLM5_VERIFY_BATCH=0 at t>1
1070 // refuses), and under the batched flag layer_batched is
1071 // unconditionally true for KDA — but neither is a structural
1072 // invariant of THIS arm (#80 review's latent-trap finding). A
1073 // sharded NO-INDEXER MLA layer legally lands here at any t
1074 // (append + truncate rollback covers every keep; foreign
1075 // geometry, never a live glm5_next path).
1076 Mixer::Kda(la) if la.tp.is_some() => {
1077 if t > 1 {
1078 return Err(format!(
1079 "glm5 verify per-row arm reached a sharded KDA \
1080 layer {il} at t={t}: no per-rank rollback stash \
1081 exists on this arm (walk-entry guard bypassed?)"
1082 )
1083 .into());
1084 }
1085 crate::glm5_tp::kda_tp_cached(
1086 e,
1087 la,
1088 &h_row,
1089 1,
1090 eps,
1091 cache,
1092 il,
1093 crate::kda::ConvArm::Decode,
1094 )?
1095 }
1096 Mixer::Mla(mla) if mla.tp.is_some() => {
1097 self.mla_tp_attn_cached(e, mla, &h_row, pos_r, 1, il, cache, false)?
1098 }
1099 Mixer::Kda(la) => {
1100 // Pre-round snapshot: ONE ssm clone per layer per round taken
1101 // before row 0 mutates the resident state (loop-port 3).
1102 if r == 0 && t > 1 {
1103 let rl = cache.recur[il]
1104 .as_ref()
1105 .ok_or("glm5 verify KDA layer has no recurrent state")?;
1106 ckpt.kda_ssm_snap[il] = Some(e.clone_dtod(&rl.ssm_state)?);
1107 }
1108 if r + 1 < t {
1109 // Steal the row's scan inputs for the replay stash (zero
1110 // copies); clone only the small conv ring per row.
1111 let (out, inputs) = crate::kda::kda_decode_cached_stash(
1112 e, la, &h_row, eps, cache, il,
1113 )?;
1114 let rl = cache.recur[il]
1115 .as_ref()
1116 .ok_or("glm5 verify KDA layer has no recurrent state")?;
1117 ckpt.kda_conv_cols[il]
1118 .get_or_insert_with(Vec::new)
1119 .push(e.clone_dtod(&rl.conv_state)?);
1120 ckpt.kda_scan_stash[il]
1121 .get_or_insert_with(Vec::new)
1122 .push(inputs);
1123 out
1124 } else {
1125 crate::kda::kda_decode_cached(e, la, &h_row, eps, cache, il)?
1126 }
1127 }
1128 Mixer::Mla(mla) => {
1129 self.mla_attn_cached(e, mla, &h_row, pos_r, 1, il, cache)?
1130 }
1131 // Refused at entry; unreachable keeps the match total without a silent arm.
1132 Mixer::Full(_) | Mixer::Linear(_) => unreachable!("refused at walk entry"),
1133 };
1134 e.copy_into(&mut mixed, r * n_embd, &out_row, n_embd)?;
1135 }
1136 mixed
1137 };
1138 x = crate::hyper::post(e, topology, &mixed, &x, &mix, t, n_embd)?;
1139
1140 let (y, mix) = crate::hyper::pre_exact(e, topology, &hyper.mlp, &x, t, n_embd)?;
1141 let mut z = e.uninit(t * n_embd)?;
1142 e.rms_norm(
1143 &y,
1144 layer.post_attn_norm.float_data(),
1145 &mut z,
1146 n_embd,
1147 t,
1148 eps,
1149 )?;
1150 // FFN branch: `batch` arms the pairs-shaped batched MoE across the t rows
1151 // (lane/glm5-vrest — fail-closed inside to the byte-identical sequential
1152 // loop); the =0 arm keeps the pre-lane per-(token,expert) class. Clocked
1153 // into the vffn sub-bucket at trace level 2 (batched arm only, like vkda).
1154 let t0 = vclock(trace_v && batch);
1155 let ffn_out = self.hyper_ffn_branch_batch(e, layer, &z, t, il, batch)?;
1156 if let Some(t0) = t0 {
1157 let _ = e.stream().synchronize();
1158 use std::sync::atomic::Ordering;
1159 crate::spec_phase::V_FFN_NS
1160 .fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1161 }
1162 x = crate::hyper::post(e, topology, &ffn_out, &x, &mix, t, n_embd)?;
1163 // glm5 DFlash2 feature tap (module doc, DRAFT SOURCE SEAM): the verify rows'
1164 // contracted completed-layer outputs are next round's drafter context.
1165 self.glm5_hc_tap(e, cache, topology, il, &x, t)?;
1166 }
1167 Ok(x)
1168 }
1169
1170 /// Write one tapped layer's CONTRACTED completed output into the armed
1171 /// [`HcTapSink`] — the glm5 DFlash2 drafter's measured feature contract (stream-mean
1172 /// over the hyper streams, the probe's `hc_contract` capture definition). Staged
1173 /// through the WALKING engine `e` (the owning stage engine under a ppN split), so the
1174 /// sink is placement-invariant. One Option check when unarmed; nothing else pays.
1175 ///
1176 /// Two staging arms (loop-port 1):
1177 /// * `device_stage` (the verify-round sink): ONE async D2D into the slot's device
1178 /// buffer — the walk never blocks; the round drains all slots post-walk in its
1179 /// single sync point (`glm5_tap_drain`). Kills the five in-walk DtoHs the 3way
1180 /// window priced into the 31.6 ms fixed round cost (map row #17).
1181 /// * host-staged (prime sinks): the pre-port behavior — per-chunk DtoH, amortized
1182 /// over the prime's >= 256-row chunks.
1183 pub(crate) fn glm5_hc_tap(
1184 &self,
1185 e: &Engine,
1186 cache: &mut Cache,
1187 topology: &crate::hyper::HyperTopology,
1188 il: usize,
1189 x: &CudaSlice<f32>,
1190 t: usize,
1191 ) -> Res<()> {
1192 let Some(sink) = cache.hc_taps.as_mut() else {
1193 return Ok(());
1194 };
1195 let base = sink.base;
1196 self.glm5_hc_tap_into(e, sink, base, topology, il, x, t)
1197 }
1198
1199 /// [`Self::glm5_hc_tap`] with the sink and its row base passed EXPLICITLY instead of read
1200 /// from the cache.
1201 ///
1202 /// The pipelined mHC prime needs this seam. Its two stage threads run different CHUNKS at
1203 /// the same moment, so a single `sink.base` field cannot describe both, and
1204 /// [`PrimeCacheStages`] hands each stage a cache shell with `hc_taps: None` — which is why
1205 /// arm 2 used to refuse outright when the DFlash2 drafter had armed a sink. Passing the
1206 /// base per call is what makes one shared sink correct for two concurrent walks.
1207 #[allow(clippy::too_many_arguments)] // allow: the list is the tap contract, base included
1208 pub(crate) fn glm5_hc_tap_into(
1209 &self,
1210 e: &Engine,
1211 sink: &mut HcTapSink,
1212 base_abs: usize,
1213 topology: &crate::hyper::HyperTopology,
1214 il: usize,
1215 x: &CudaSlice<f32>,
1216 t: usize,
1217 ) -> Res<()> {
1218 let Some(slot) = sink.layer_ids.iter().position(|&l| l == il) else {
1219 return Ok(());
1220 };
1221 let saved = sink.base;
1222 sink.base = base_abs;
1223 let out = self.glm5_hc_tap_slot(e, sink, slot, topology, x, t);
1224 sink.base = saved;
1225 out
1226 }
1227
1228 fn glm5_hc_tap_slot(
1229 &self,
1230 e: &Engine,
1231 sink: &mut HcTapSink,
1232 slot: usize,
1233 topology: &crate::hyper::HyperTopology,
1234 x: &CudaSlice<f32>,
1235 t: usize,
1236 ) -> Res<()> {
1237 let h = sink.hidden;
1238 let n_taps = sink.layer_ids.len();
1239 // Sink-relative row of this walk's row 0 (doc on `HcTapSink::origin`): fresh-prompt
1240 // sinks have origin 0 and this is exactly the pre-field arithmetic; a suffix-prime
1241 // sink is anchored at the restored boundary. A base below the origin is a caller
1242 // bug (a walk over rows the sink does not cover) — refuse, never wrap.
1243 let base = sink.base.checked_sub(sink.origin).ok_or_else(|| {
1244 format!(
1245 "hc tap base {} below sink origin {} (walk outside the sink's window)",
1246 sink.base, sink.origin,
1247 )
1248 })?;
1249 debug_assert!(
1250 base + t <= sink.t,
1251 "hc tap window {base}+{t} exceeds sink {}",
1252 sink.t
1253 );
1254 let contracted = crate::hyper::contract_mean(e, topology, x, t, h)?;
1255 if sink.device_stage {
1256 // Lazy slot buffer on the WRITING engine (this layer always walks on one
1257 // stage, so the buffer's device is stable for the sink's lifetime). Every
1258 // walk row writes every tapped layer, so the buffer is fully covered by the
1259 // walk that armed the sink.
1260 if sink.dev[slot].is_none() {
1261 sink.dev[slot] = Some(e.uninit(sink.t * h)?);
1262 }
1263 let buf = sink.dev[slot].as_mut().expect("just filled");
1264 e.copy_into(buf, base * h, &contracted, t * h)?;
1265 return Ok(());
1266 }
1267 let t_dtoh = std::time::Instant::now();
1268 let host = e.dtoh(&contracted)?;
1269 for r in 0..t {
1270 let dst = (base + r) * n_taps * h + slot * h;
1271 sink.rows[dst..dst + h].copy_from_slice(&host[r * h..(r + 1) * h]);
1272 }
1273 sink.dtoh_ns += t_dtoh.elapsed().as_nanos() as u64;
1274 Ok(())
1275 }
1276
1277 /// Drain a device-staged tap sink into its host `rows` — the round's ONE post-walk
1278 /// sync point for tap features (loop-port 1). Each slot reads back through its
1279 /// layer's OWNING engine (the stage engine under a live split, the caller's engine
1280 /// otherwise); the verify walk's terminal drain has already retired every stage's
1281 /// writes (stream program order: the slot copy precedes its stage's TX, and the
1282 /// TX-wait chain covers it transitively — the pp.rs multi-stream law).
1283 fn glm5_tap_drain(&self, e: &Engine, sink: &mut HcTapSink) -> Res<()> {
1284 if !sink.device_stage {
1285 return Ok(());
1286 }
1287 let h = sink.hidden;
1288 let n_taps = sink.layer_ids.len();
1289 let split = match crate::pp::pp_cuts(self.layers.len()) {
1290 Some(fence) if !crate::pp::pp2_streams_off() => {
1291 Some((crate::pp::PpNRt::get(e)?, fence))
1292 }
1293 _ => None,
1294 };
1295 for slot in 0..n_taps {
1296 let Some(buf) = sink.dev[slot].take() else {
1297 continue;
1298 };
1299 let il = sink.layer_ids[slot];
1300 let es = match split.as_ref() {
1301 Some((rt, fence)) => {
1302 let stage = fence
1303 .windows(2)
1304 .position(|w| il >= w[0] && il < w[1])
1305 .ok_or_else(|| format!("tap layer {il} outside every stage range"))?;
1306 rt.engine(stage, e)
1307 }
1308 None => e,
1309 };
1310 let host = es.dtoh(&buf)?;
1311 for r in 0..sink.t {
1312 let dst = r * n_taps * h + slot * h;
1313 sink.rows[dst..dst + h].copy_from_slice(&host[r * h..(r + 1) * h]);
1314 }
1315 }
1316 Ok(())
1317 }
1318
1319 /// Trunk exit of the verify walk, the batched head's decode-exact form
1320 /// (`hyper_batch_head_logits`), with the collapsed pre-output_norm rows kept — they are
1321 /// the h_seeds the MTP head re-seeds from (LANE.md §A). Under a split this runs on the
1322 /// LAST stage's engine, where the loader put `output_norm` + the lm head.
1323 fn glm5_verify_head(
1324 &self,
1325 e: &Engine,
1326 topology: &crate::hyper::HyperTopology,
1327 x: &CudaSlice<f32>,
1328 t: usize,
1329 ) -> Res<(CudaSlice<f32>, CudaSlice<f32>)> {
1330 let n_embd = self.cfg.n_embd as usize;
1331 let eps = self.cfg.rms_eps;
1332 let collapsed =
1333 crate::hyper::collapse(e, topology, self.hyper_head.as_ref(), x, t, n_embd)?;
1334 let mut hn = e.uninit(t * n_embd)?;
1335 e.rms_norm(
1336 &collapsed,
1337 self.output_norm.float_data(),
1338 &mut hn,
1339 n_embd,
1340 t,
1341 eps,
1342 )?;
1343 // Under the batched walk the lm head rides the rows-exact classes too (the bf16
1344 // tcols twin reads the 1.27 GB head ONCE per round instead of once per row);
1345 // per-row bits unchanged by contract, the tcols bit-gate holds it.
1346 let logits = if glm5_verify_batch_on() && t > 1 {
1347 e.matmul_rows_exact(&self.output, &hn, t)?
1348 } else {
1349 e.matmul_decode_exact(&self.output, &hn, t)?
1350 };
1351 Ok((logits, collapsed))
1352 }
1353
1354 /// ppN twin of the verify walk (lane/glm5-ppn-verify, 2026-08-30), mirroring
1355 /// `decode_step_batch_hyper_ppn` (decode_batch.rs): the t=K+1 rows walk as N stage
1356 /// subgraphs — per-stage engine, per-stage pos_rows uploads, ONE `[t, streams, n_embd]`
1357 /// boundary payload per fence cut. Row chaining is per-LAYER through the one cache
1358 /// (row r+1 at layer il depends only on row r at layer il), so a straight layer-range
1359 /// split preserves it exactly; no row ever crosses a boundary individually. Head +
1360 /// collapsed rows land on the LAST stage's engine — where the loader put the lm head
1361 /// and where `pp::new_cache*` places the MTP plane the re-seed feeds.
1362 ///
1363 /// DRAIN CONTRACT: this walk returns DEVICE buffers with no terminal dtoh (unlike its
1364 /// decode twins, whose epilogue reads back on the last stage's stream), so it owns the
1365 /// settle — the per-stage arm synchronizes the LAST stage's stream before returning.
1366 /// The TX-wait chain transitively covers every earlier stage (pp.rs multi-stream law),
1367 /// so the logits, the collapsed rows AND the ckpt's per-stage KDA columns are all safe
1368 /// for consumption from the caller's streams after this returns.
1369 #[allow(clippy::too_many_arguments)]
1370 // allow: the parameter list mirrors its decode twin's stage-walk contract
1371 fn glm5_verify_rows_ppn(
1372 &self,
1373 e: &Engine,
1374 tokens: &[u32],
1375 cache: &mut Cache,
1376 mut ckpt: Glm5VerifyCkpt,
1377 topology: &crate::hyper::HyperTopology,
1378 fence: &[usize],
1379 ) -> Res<(CudaSlice<f32>, CudaSlice<f32>, Glm5VerifyCkpt)> {
1380 let t = tokens.len();
1381 let n_embd = self.cfg.n_embd as usize;
1382 let pos0 = ckpt.pos;
1383 let payload = t * topology.streams * n_embd;
1384 // Position buffers through THIS stage's engine (the per-stage pos_d law:
1385 // allocated, consumed and freed on one stage's stream).
1386 let pos_on = |eng: &Engine| -> Res<Glm5VerifyPos> { Glm5VerifyPos::new(eng, pos0, t) };
1387
1388 // Same-stream seam (MEMRA_PP_STREAMS=0): one engine, boundary copies between
1389 // ranges — the shape every hc ppN walk uses for this knob.
1390 if crate::pp::pp2_streams_off() {
1391 let pos = pos_on(e)?;
1392 let embedded = e.htod(&self.embd.try_gather(n_embd, tokens)?)?;
1393 let mut x = crate::hyper::expand(e, topology, &embedded, t, n_embd)?;
1394 x =
1395 self.glm5_verify_range(e, topology, x, fence[0], fence[1], &pos, cache, &mut ckpt)?;
1396 for s in 1..fence.len() - 1 {
1397 let boundary_tx = e.clone_dtod(&x)?;
1398 let boundary_rx = e.clone_dtod(&boundary_tx)?;
1399 x = self.glm5_verify_range(
1400 e,
1401 topology,
1402 boundary_rx,
1403 fence[s],
1404 fence[s + 1],
1405 &pos,
1406 cache,
1407 &mut ckpt,
1408 )?;
1409 }
1410 let (logits, collapsed) = self.glm5_verify_head(e, topology, &x, t)?;
1411 return Ok((logits, collapsed, ckpt));
1412 }
1413
1414 let rt = crate::pp::PpNRt::get(e)?;
1415 let n_st = fence.len() - 1;
1416 assert_eq!(
1417 rt.n_stages(),
1418 n_st,
1419 "PpNRt stage count {} != fence stages {n_st}",
1420 rt.n_stages()
1421 );
1422 // #87 reverse publication (see decode_step_batch_ppn): order every stage stream
1423 // behind the caller before this body's first stage allocation.
1424 rt.fence_stages_behind(&e.stream())?;
1425
1426 // ---- STAGE 0: embed + expand (no weights) + layers [0, fence[1]) + TX ----
1427 let mut slot = {
1428 let _st0 = rt.enter(0);
1429 let e0 = rt.engine(0, e);
1430 let pos = pos_on(e0)?;
1431 let embedded = e0.htod(&self.embd.try_gather(n_embd, tokens)?)?;
1432 let x = crate::hyper::expand(e0, topology, &embedded, t, n_embd)?;
1433 let x = self
1434 .glm5_verify_range(e0, topology, x, fence[0], fence[1], &pos, cache, &mut ckpt)?;
1435 rt.tx(0, &x, payload)?
1436 };
1437
1438 // ---- MIDDLE STAGES: RX -> range -> TX ----
1439 for s in 1..n_st - 1 {
1440 let _st = rt.enter(s);
1441 let es = rt.engine(s, e);
1442 let pos = pos_on(es)?;
1443 let x = rt.rx(s - 1, slot, payload)?;
1444 let x = self.glm5_verify_range(
1445 es,
1446 topology,
1447 x,
1448 fence[s],
1449 fence[s + 1],
1450 &pos,
1451 cache,
1452 &mut ckpt,
1453 )?;
1454 slot = rt.tx(s, &x, payload)?;
1455 }
1456
1457 // ---- LAST STAGE: RX + final range + collapse/head + the drain (doc above) ----
1458 let _stl = rt.enter(n_st - 1);
1459 let el = rt.engine(n_st - 1, e);
1460 let pos = pos_on(el)?;
1461 let x = rt.rx(n_st - 2, slot, payload)?;
1462 let x = self.glm5_verify_range(
1463 el,
1464 topology,
1465 x,
1466 fence[n_st - 1],
1467 fence[n_st],
1468 &pos,
1469 cache,
1470 &mut ckpt,
1471 )?;
1472 let (logits, collapsed) = self.glm5_verify_head(el, topology, &x, t)?;
1473 // el.stream() under the enter-guard IS the stage stream (memra_runtime ambient
1474 // override) — this drain settles the whole walk transitively.
1475 el.stream().synchronize()?;
1476 drop(_stl);
1477 // EXIT PUBLICATION (lane/glm5-accrace): the drain above settles the LAST stage, and
1478 // the TX-wait chain covers every earlier stage's work only UP TO its `ev_tx`. Each
1479 // earlier stage's stream still holds the stage-scope tail its locals enqueue when
1480 // they drop under the override (`pos`, the boundary residual, the per-layer
1481 // transients, this round's ckpt clones). The caller resumes and allocates for the
1482 // accept walk and the MTP re-seed, so it must be ordered behind ALL stages.
1483 self.glm5_publish_stages(e)?;
1484 Ok((logits, collapsed, ckpt))
1485 }
1486
1487 /// The engine that owns the trunk exit (collapse + output_norm + lm head), the MTP
1488 /// block's weights AND its latent plane under a ppN split: the LAST stage's engine —
1489 /// `hybrid.rs` uploads the head there (`pp::layer_engine(e, n_trunk, n_trunk - 1)`)
1490 /// and `pp::new_cache*` maps trailing MTP/NextN planes to the last stage. Door shut or
1491 /// the same-stream seam: the caller's engine, unchanged (single-device callers pay
1492 /// nothing — `e` is returned by identity).
1493 fn glm5_head_engine<'e>(&self, e: &'e Engine) -> Res<&'e Engine> {
1494 match crate::pp::pp_cuts(self.layers.len()) {
1495 Some(fence) if !crate::pp::pp2_streams_off() => {
1496 let rt = crate::pp::PpNRt::get(e)?;
1497 Ok(rt.engine(fence.len() - 2, e))
1498 }
1499 _ => Ok(e),
1500 }
1501 }
1502
1503 /// Roll the trunk back to exactly `keep` accepted verify rows (1 <= keep <= t; keep =
1504 /// j+1: the always-committed anchor row plus j accepted drafts).
1505 ///
1506 /// - MLA latent planes: `len = snapshot + keep` (truncate; rows are position-addressed
1507 /// and append-only, so the kept rows ARE what a plain decode chain would have
1508 /// written — the decode-exact contract), device `len_d` in lock-step, and
1509 /// `truncate_index_pool_keys(pool)` clamps pool-key finality to what the shortened
1510 /// `len` still justifies (the tail-ring residency tripwire fires on the next call if
1511 /// this clamp is ever skipped).
1512 /// - KDA state: restore column keep-1 (state after the last kept row); full accept
1513 /// (keep == t) keeps the resident state — the columns are clones OF it.
1514 /// - `cache.pos = snapshot + keep`.
1515 ///
1516 /// Under a live ppN split each stage's layers restore THROUGH that stage's engine ON
1517 /// its stream: the state planes and the ckpt columns live on the owning stage's device
1518 /// (per-stage `KvDev` allocation; per-stage clones in the walk), and enqueuing the
1519 /// restores on the same stage streams the walk writes on orders them relative to the
1520 /// walk without any extra fence.
1521 ///
1522 /// THE EXIT PUBLICATION IS NOT OPTIONAL (lane/glm5-accrace 2026-09-01). This body used
1523 /// to return with the restores merely ENQUEUED on the stage streams, on the reasoning
1524 /// that "the next walk's own entry fence covers the primary-stream seam". It does not:
1525 /// `fence_stages_behind` orders the STAGE streams behind the CALLER, and everything the
1526 /// round does after this point — the MTP plane reset, the h_seed rows, the next round's
1527 /// whole draft chain, the next SESSION's cache allocation and prime — runs on the
1528 /// CALLER's stream and ALLOCATES. cudarc's drops carry no read guard, so the pool could
1529 /// hand the caller a block whose stage-stream lifetime had not retired and the caller's
1530 /// writes landed under queued rollback work.
1531 ///
1532 /// MEASURED CONSEQUENCE, and why a "rollback ordering" bug showed up as a PRIME bug:
1533 /// with per-stage streams on one device the hc ppN prime over a fixed 24-token prompt
1534 /// returned three distinct logit fingerprints inside one process (a third of all primes
1535 /// non-canonical); downstream, one glm5 spec round lost an acceptance silently
1536 /// (14/42 -> 13/42) and the e2e tape diverged. Publishing here took non-canonical primes
1537 /// from 20/110 to 2/110 in an interleaved A/B, and the walk's own exit publication
1538 /// closed the remainder. Receipts:
1539 /// `research/glm53-flash-bringup-20260827/accrace-20260901/LANE.md`.
1540 pub fn glm5_verify_rollback(
1541 &self,
1542 e: &Engine,
1543 cache: &mut Cache,
1544 ckpt: &Glm5VerifyCkpt,
1545 keep: usize,
1546 ) -> Res<()> {
1547 if keep == 0 || keep > ckpt.rows {
1548 return Err(format!(
1549 "glm5_verify_rollback: keep={keep} outside 1..={} (the anchor row is always \
1550 committed; keep = accepted drafts + 1)",
1551 ckpt.rows
1552 )
1553 .into());
1554 }
1555 match crate::pp::pp_cuts(self.layers.len()) {
1556 Some(fence) if !crate::pp::pp2_streams_off() => {
1557 let rt = crate::pp::PpNRt::get(e)?;
1558 for s in 0..fence.len() - 1 {
1559 let _st = rt.enter(s);
1560 let es = rt.engine(s, e);
1561 for il in fence[s]..fence[s + 1] {
1562 self.glm5_rollback_layer(es, cache, ckpt, keep, il)?;
1563 }
1564 }
1565 // EXIT PUBLICATION (doc above): every stage stream, to the caller's.
1566 self.glm5_publish_stages(e)?;
1567 }
1568 _ => {
1569 for il in 0..self.layers.len() {
1570 self.glm5_rollback_layer(e, cache, ckpt, keep, il)?;
1571 }
1572 }
1573 }
1574 cache.pos = ckpt.pos + keep;
1575 Ok(())
1576 }
1577
1578 /// Restore ONE trunk layer to the ckpt's `keep`-row state (the per-plane contract in
1579 /// [`Self::glm5_verify_rollback`]'s doc). `e` is the layer's OWNING engine — the stage
1580 /// engine under a split, the caller's engine otherwise.
1581 fn glm5_rollback_layer(
1582 &self,
1583 e: &Engine,
1584 cache: &mut Cache,
1585 ckpt: &Glm5VerifyCkpt,
1586 keep: usize,
1587 il: usize,
1588 ) -> Res<()> {
1589 match &self.layers[il].mixer {
1590 Mixer::Mla(mla) => {
1591 if keep == ckpt.rows {
1592 // Full accept: the walk already advanced len AND the len_d device
1593 // mirror to saved + rows on the canonical plane and every replica
1594 // (append-time stores), so the restore below would rewrite unchanged
1595 // values — ~11 synchronizing pageable 4-byte copies per round on the
1596 // HOT outcome (the KDA arms' early-out twin; #82 review).
1597 return Ok(());
1598 }
1599 let saved = ckpt.latent_len[il].ok_or_else(|| {
1600 format!("glm5_verify_rollback: MLA layer {il} missing from the ckpt")
1601 })?;
1602 let plane = cache.latent[il].as_mut().ok_or_else(|| {
1603 format!("glm5_verify_rollback: MLA layer {il} has no latent plane")
1604 })?;
1605 plane.len = saved + keep;
1606 let len_i32 =
1607 i32::try_from(plane.len).map_err(|_| "latent length exceeds i32 mirror")?;
1608 // Door H (`MEMRA_GLM5_HTOD_DIET`): async `i32_set_k` instead of the synchronizing
1609 // pageable 4-byte copy — 11 of these per round, and unconditional (unlike the
1610 // KDA arm, which short-circuits when `keep == rows`).
1611 e.i32_mirror_store(&mut plane.len_d, len_i32)?;
1612 if let Some(indexer) = mla.index.as_ref() {
1613 plane.truncate_index_pool_keys(indexer.geom.pool);
1614 }
1615 // spec x TP composition: the PEER latent replicas append in lock-step with
1616 // the canonical plane (the TP walk's construction), so the same truncation
1617 // restores each of them — through its own rank's engine for the device
1618 // `len_d` mirror. Full accept skips the loop (lens already read
1619 // saved + rows — the KDA arm's early-out twin; each skipped store is a
1620 // synchronizing pageable copy per rank per layer on the HOT outcome).
1621 // Missing replicas after a verify walk are a wiring bug and refuse by
1622 // name, never a silent canonical-only restore (#80 review hardening).
1623 if let Some(tp) = mla.tp.as_ref() {
1624 let replicas = cache.glm5_tp_latent_peer[il].as_mut().ok_or_else(|| {
1625 format!(
1626 "glm5_verify_rollback: sharded MLA layer {il} has no peer \
1627 latent replicas (the TP verify walk hydrates them; a \
1628 rollback without them would silently restore the canonical \
1629 plane only)"
1630 )
1631 })?;
1632 for (i, replica) in replicas.iter_mut().enumerate() {
1633 replica.len = saved + keep;
1634 tp.rt.peers[i].i32_mirror_store(&mut replica.len_d, len_i32)?;
1635 if let Some(indexer) = mla.index.as_ref() {
1636 replica.truncate_index_pool_keys(indexer.geom.pool);
1637 }
1638 }
1639 }
1640 }
1641 Mixer::Kda(la) if la.tp.is_some() => {
1642 if keep == ckpt.rows {
1643 return Ok(()); // resident per-rank states ARE the post-keep states
1644 }
1645 let stash = ckpt.kda_tp[il].as_ref().ok_or_else(|| {
1646 format!(
1647 "glm5_verify_rollback: sharded KDA layer {il} has no per-rank stash \
1648 (the batched TP verify walk fills it; the per-row arm is refused \
1649 at walk entry)"
1650 )
1651 })?;
1652 crate::glm5_tp::kda_tp_verify_rollback(e, la, stash, keep, cache, il)?;
1653 }
1654 Mixer::Kda(la) => {
1655 if keep == ckpt.rows {
1656 return Ok(()); // resident state IS the state after the last kept row
1657 }
1658 // BATCHED-walk stash (lane/glm5-verify-batch): ring restore + re-roll,
1659 // then ONE scan replay at T=keep from the pre-round snapshot.
1660 if let Some(stash) = ckpt.kda_rows[il].as_ref() {
1661 let snap = ckpt.kda_ssm_snap[il].as_ref().ok_or_else(|| {
1662 format!("glm5_verify_rollback: KDA layer {il} has no ssm snapshot")
1663 })?;
1664 return crate::kda::kda_verify_rollback_rows(
1665 e, la, snap, stash, keep, cache, il,
1666 );
1667 }
1668 // Conv ring: restore the cloned column (unchanged — 288 KiB).
1669 let conv_cols = ckpt.kda_conv_cols[il].as_ref().ok_or_else(|| {
1670 format!("glm5_verify_rollback: KDA layer {il} has no conv columns")
1671 })?;
1672 let conv = &conv_cols[keep - 1];
1673 {
1674 let rl = cache.recur[il].as_mut().ok_or_else(|| {
1675 format!("glm5_verify_rollback: KDA layer {il} has no recurrent state")
1676 })?;
1677 e.copy_into(&mut rl.conv_state, 0, conv, conv.len())?;
1678 }
1679 // Recurrent state: REPLAY rows 0..keep from the pre-round snapshot
1680 // (loop-port 3; ckpt doc) — each replay re-issues that row's original
1681 // t=1 scan over its stolen inputs, so the rebuilt state is byte-identical
1682 // to the per-row clone this retires.
1683 let snap = ckpt.kda_ssm_snap[il].as_ref().ok_or_else(|| {
1684 format!("glm5_verify_rollback: KDA layer {il} has no ssm snapshot")
1685 })?;
1686 let stash = ckpt.kda_scan_stash[il].as_ref().ok_or_else(|| {
1687 format!("glm5_verify_rollback: KDA layer {il} has no scan stash")
1688 })?;
1689 crate::kda::kda_scan_replay(e, la, snap, &stash[..keep], cache, il)?;
1690 }
1691 Mixer::Full(_) | Mixer::Linear(_) => {
1692 return Err(format!(
1693 "glm5_verify_rollback: layer {il} mixer class was refused at walk \
1694 entry and cannot appear in a ckpt"
1695 )
1696 .into());
1697 }
1698 }
1699 Ok(())
1700 }
1701
1702 /// Reset the MTP draft plane (il = n_trunk) to `len` rows — the LANE.md rollback
1703 /// contract ("one row per step, rollback = plane len reset") plus the same pool-key
1704 /// clamp every len-shortening path owes the tail ring. The plane lives on the LAST
1705 /// stage under a split (`pp::new_cache*` maps trailing MTP planes there), so the
1706 /// device mirror writes through the head engine.
1707 fn glm5_mtp_plane_reset(&self, e: &Engine, cache: &mut Cache, len: usize) -> Res<()> {
1708 let e = self.glm5_head_engine(e)?;
1709 let mtp = self
1710 .mtp
1711 .as_ref()
1712 .ok_or("glm5_mtp_plane_reset with no MTP head loaded")?;
1713 let il = self
1714 .plan
1715 .mtp_blocks
1716 .first()
1717 .ok_or("ModelPlan declares no MTP block")?
1718 .layer
1719 .index as usize;
1720 let plane = cache
1721 .latent
1722 .get_mut(il)
1723 .and_then(|plane| plane.as_mut())
1724 .ok_or_else(|| format!("MTP block layer {il} has no latent cache plane"))?;
1725 if len > plane.len {
1726 return Err(format!(
1727 "glm5_mtp_plane_reset: target {len} is past the plane's {} rows — a reset \
1728 only ever shortens",
1729 plane.len
1730 )
1731 .into());
1732 }
1733 plane.len = len;
1734 let len_i32 = i32::try_from(len).map_err(|_| "latent length exceeds i32 mirror")?;
1735 e.stream().memcpy_htod(&[len_i32], &mut plane.len_d)?;
1736 if let Mixer::Mla(mla) = &mtp.mixer
1737 && let Some(indexer) = mla.index.as_ref()
1738 {
1739 plane.truncate_index_pool_keys(indexer.geom.pool);
1740 }
1741 Ok(())
1742 }
1743
1744 /// Single-shot glm5 speculative generation: draft (MTP head, K steps) -> verify (one
1745 /// t=K+1 walk) -> accept j (greedy longest matching prefix) -> rollback -> re-seed.
1746 /// Returns `(tokens, drafted, accepted)` — `generate_spec`'s contract. One-shot form:
1747 /// builds a [`Glm5SpecSession`] over a fresh cache and drives it to `max_new` — the
1748 /// SAME round machinery the serve worker bursts, so the tparallel gate's byte-identity
1749 /// pins cover the served path's rounds too.
1750 pub fn generate_spec_glm5(
1751 &self,
1752 e: &Engine,
1753 prompt: &[u32],
1754 max_new: usize,
1755 k: usize,
1756 ) -> Res<(Vec<u32>, usize, usize)> {
1757 self.generate_spec_glm5_gated(e, prompt, max_new, k, Glm5SpecKnobs::default())
1758 }
1759
1760 /// `generate_spec_glm5` with GATE INSTRUMENTS (never a serving surface): a draft
1761 /// override for deterministic forced-accept / forced-reject rounds, and a
1762 /// rollback-disable arm that red-proves the end-to-end byte-identity gate.
1763 pub fn generate_spec_glm5_gated(
1764 &self,
1765 e: &Engine,
1766 prompt: &[u32],
1767 max_new: usize,
1768 k: usize,
1769 mut knobs: Glm5SpecKnobs<'_>,
1770 ) -> Res<(Vec<u32>, usize, usize)> {
1771 let cap = Self::hyper_batch_cap();
1772 if k == 0 || k + 1 > cap {
1773 return Err(format!(
1774 "generate_spec_glm5: k={k} outside 1..={} (verify rows = k+1 must stay \
1775 inside the decode-exact knee, cap {cap})",
1776 cap - 1
1777 )
1778 .into());
1779 }
1780 if max_new == 0 {
1781 return Ok((Vec::new(), 0, 0));
1782 }
1783 let max_ctx = prompt.len() + max_new + k + 8;
1784 let mut sess = self.glm5_spec_session_new(e, prompt, max_ctx, None)?;
1785 let mut out: Vec<u32> = Vec::with_capacity(max_new + k);
1786 let mut drafted = 0usize;
1787 let mut accepted = 0usize;
1788 while out.len() < max_new && !sess.finished() {
1789 let (burst, d, a) = self.glm5_spec_session_burst_gated(
1790 e,
1791 &mut sess,
1792 max_new - out.len(),
1793 k,
1794 &[],
1795 &mut knobs,
1796 )?;
1797 if burst.is_empty() {
1798 break; // ctx guard tripped with nothing new — never spin
1799 }
1800 out.extend(burst);
1801 drafted += d;
1802 accepted += a;
1803 }
1804 out.truncate(max_new);
1805 Ok((out, drafted, accepted))
1806 }
1807
1808 /// BATCHED MTP-PLANE WARM (loop-port fold-in; doc at the call site in
1809 /// `glm5_spec_session_new`): fill the NextN block's latent plane with rows for pairs
1810 /// `(tokens_next[i], hiddens row i)`, i in `0..t`, in chunked t-parallel passes —
1811 /// ops 1-7 of `mtp_head_forward_mla_cached` batched over the chunk (embed gather,
1812 /// enorm/hnorm, the eh_proj concat via `place_rows_strided`, attn_norm), then ONE
1813 /// `mla_attn_cached` append per chunk (the prime-class t>1 arm the trunk's own MLA
1814 /// layers warm through; its attention output is discarded — the plane rows are the
1815 /// product). The MoE FFN, final norm and lm-head of the per-token chain are never
1816 /// run: they fed nothing but the (discarded) draft logits of prompt positions.
1817 fn glm5_mtp_plane_fill(
1818 &self,
1819 e: &Engine,
1820 tokens_next: &[u32],
1821 hiddens: &CudaSlice<f32>,
1822 t: usize,
1823 cache: &mut Cache,
1824 ) -> Res<()> {
1825 let mtp = self
1826 .mtp
1827 .as_ref()
1828 .ok_or("glm5_mtp_plane_fill with no MTP head loaded")?;
1829 let il = self
1830 .plan
1831 .mtp_blocks
1832 .first()
1833 .ok_or("ModelPlan declares no MTP block")?
1834 .layer
1835 .index as usize;
1836 let Mixer::Mla(mla) = &mtp.mixer else {
1837 return Err("glm5_mtp_plane_fill serves MLA-mixer MTP blocks only".into());
1838 };
1839 if tokens_next.len() < t {
1840 return Err(format!(
1841 "glm5_mtp_plane_fill: {t} rows requested over {} successor tokens",
1842 tokens_next.len()
1843 )
1844 .into());
1845 }
1846 let n_embd = self.cfg.n_embd as usize;
1847 let eps = self.cfg.rms_eps;
1848 // Chunk bound: the trunk prime's workspace discipline — bounds the t>1 attention
1849 // workspace and the transient buffers below without changing the append semantics
1850 // (`mla_attn_cached` appends at the plane's running length either way).
1851 const CHUNK: usize = 512;
1852 let mut done = 0usize;
1853 while done < t {
1854 let tc = (t - done).min(CHUNK);
1855 let e_emb = e.htod(
1856 &self
1857 .embd
1858 .try_gather(n_embd, &tokens_next[done..done + tc])?,
1859 )?;
1860 let mut e_norm = e.uninit(tc * n_embd)?;
1861 e.rms_norm(&e_emb, mtp.enorm.float_data(), &mut e_norm, n_embd, tc, eps)?;
1862 // hnorm over the chunk's hidden rows (one contiguous view copy — rms_norm
1863 // takes an owned-slice operand).
1864 let hv = e.view(hiddens, (done + tc) * n_embd);
1865 let mut h_rows = e.uninit(tc * n_embd)?;
1866 e.copy_view_into(
1867 &mut h_rows,
1868 0,
1869 &hv.slice(done * n_embd..(done + tc) * n_embd),
1870 tc * n_embd,
1871 )?;
1872 let mut h_norm = e.uninit(tc * n_embd)?;
1873 e.rms_norm(
1874 &h_rows,
1875 mtp.hnorm.float_data(),
1876 &mut h_norm,
1877 n_embd,
1878 tc,
1879 eps,
1880 )?;
1881 // concat rows [tc, 2*n_embd] = [enorm ; hnorm] — two strided placements.
1882 let mut concat = e.uninit(tc * 2 * n_embd)?;
1883 e.place_rows_strided(&e_norm, &mut concat, n_embd, tc, 2 * n_embd, 0)?;
1884 e.place_rows_strided(&h_norm, &mut concat, n_embd, tc, 2 * n_embd, n_embd)?;
1885 let inp_sa = e.matmul(&mtp.eh_proj, &concat, tc)?;
1886 let mut a_norm = e.uninit(tc * n_embd)?;
1887 e.rms_norm(
1888 &inp_sa,
1889 mtp.attn_norm.float_data(),
1890 &mut a_norm,
1891 n_embd,
1892 tc,
1893 eps,
1894 )?;
1895 let pos: Vec<i32> = (done as i32..(done + tc) as i32).collect();
1896 let pos_d = e.htod_i32(&pos)?;
1897 let _ = self.mla_attn_cached(e, mla, &a_norm, &pos_d, tc, il, cache)?;
1898 done += tc;
1899 }
1900 Ok(())
1901 }
1902
1903 /// Row `row` of a `[rows, n_embd]` device stack, copied into its own `[n_embd]` buffer
1904 /// (the MTP `h_seed` handoff shape).
1905 fn glm5_seed_row(
1906 &self,
1907 e: &Engine,
1908 src: &CudaSlice<f32>,
1909 rows: usize,
1910 row: usize,
1911 ) -> Res<CudaSlice<f32>> {
1912 let n_embd = self.cfg.n_embd as usize;
1913 let stack = e.view(src, rows * n_embd);
1914 let view = stack.slice(row * n_embd..(row + 1) * n_embd);
1915 let mut seed = e.uninit(n_embd)?;
1916 e.copy_view_into(&mut seed, 0, &view, n_embd)?;
1917 Ok(seed)
1918 }
1919
1920 /// SERVED-SESSION ENTRY (lane/glm5-spec-routing): prime the prompt, warm the MTP plane,
1921 /// draw the boundary token, and hand back a [`Glm5SpecSession`] the worker bursts.
1922 ///
1923 /// `sampling`: `None` / `temp <= 0` = the greedy byte-contract route (the instrument);
1924 /// `Some` with `temp > 0` = the sampled route — the boundary token, the draft chain and
1925 /// the accept walk all draw through the session's own Philox counters (`sctr` device
1926 /// events, `uctr` host accept-test uniforms via `spec::host_u01`, tag 0xFFFF_FFFE), so a
1927 /// session's randomness never repeats across bursts (the session-continuity law).
1928 /// PENALIZED requests (greedy or sampled) open only under `MEMRA_SPEC_PENALTY=1`
1929 /// (`glm5_penalty_admit`; doc on [`glm5_spec_penalty_on`]) and refuse loudly otherwise —
1930 /// worker admission keeps them on the plain path while the arm is dark.
1931 pub fn glm5_spec_session_new(
1932 &self,
1933 e: &Engine,
1934 prompt: &[u32],
1935 ctx_cap: usize,
1936 sampling: Option<SpecSampling>,
1937 ) -> Res<Glm5SpecSession> {
1938 if self.hyper.is_none() {
1939 return Err("generate_spec_glm5 requires a HyperConnections trunk".into());
1940 }
1941 // Two parallel/spec programs on one model never silently coexist unless the
1942 // composition is EXPLICITLY armed: the spec x TP verify/rollback wiring
1943 // (lane/glm5-composition) exists and is rig-gated, but it has zero real-artifact
1944 // receipts, so sessions on a SHARDED model stay co-refused unless
1945 // MEMRA_GLM5_SPEC_TP=1 lifts the refusal (default OFF by design — the FLAGS row).
1946 // The predicate is the MODEL's own sharding (the same per-layer truth the verify
1947 // walk keys on), never the MEMRA_GLM5_TP env: sharding is a load-time property,
1948 // and an env read here is bypassable after load (set/load/unset) and spuriously
1949 // refuses an UNSHARDED model in a process that still carries the env (the #80
1950 // review's confirmed finding).
1951 let tp_sharded = self.layers.iter().any(|l| match &l.mixer {
1952 Mixer::Kda(la) => la.tp.is_some(),
1953 Mixer::Mla(mla) => mla.tp.is_some(),
1954 _ => false,
1955 });
1956 if tp_sharded {
1957 if !glm5_spec_tp_on() {
1958 return Err(
1959 "glm5 spec is co-refused on a MEMRA_GLM5_TP-sharded model: set \
1960 MEMRA_GLM5_SPEC_TP=1 to run the gated spec x TP composition \
1961 (default OFF — zero real-artifact receipts; every other admission \
1962 law still holds)"
1963 .into(),
1964 );
1965 }
1966 if !glm5_verify_batch_on() {
1967 return Err("MEMRA_GLM5_SPEC_TP=1 requires the BATCHED verify walk \
1968 (MEMRA_GLM5_VERIFY_BATCH must not be 0): the per-row rollback seam \
1969 carries no TP arm"
1970 .into());
1971 }
1972 // The ARMED announce prints AFTER the last admission law below — a session
1973 // refused later (draft source, ppN qualification, penalties, ctx) must never
1974 // log the engagement receipt (the fleet's prints-ARMED-then-serves-plain
1975 // trap class; rig-gates/03 caught SF3 logging it on a refused session).
1976 }
1977 // DRAFT-SOURCE SELECTION — through the GENERAL law
1978 // ([`crate::spec::resolve_draft_source_kind`], lane/glm5-extract2): DFlash2 when the
1979 // drafter is loaded (MEMRA_GLM5_DFLASH — it wins over a co-loaded MTP head, the boot
1980 // receipt states the selection), native MTP otherwise; neither = the loud refusal
1981 // below, whose bytes name the glm5 flags because the FAMILY owns the how-to-arm text.
1982 // The native MTP head is NOT required for the DFlash2 source (the q38 pattern).
1983 let dflash_src = self.glm5_dflash.as_ref();
1984 let source_kind = crate::spec::resolve_draft_source_kind(
1985 self.plan.draft_source,
1986 self.mtp.is_some(),
1987 dflash_src.is_some(),
1988 )
1989 .map_err(|why| {
1990 // The general law says WHY there is no usable source; the family owns the
1991 // how-to-arm text. Composed so the sentence is true on BOTH of the law's refusal
1992 // branches (nothing loaded / a head loaded under a plan that does not claim it),
1993 // rather than asserting "requires a draft source" at an operator whom the bracket
1994 // then tells a head IS loaded.
1995 format!(
1996 "generate_spec_glm5 cannot select a draft source ({why}). Arm one: the \
1997 embedded MTP head (MEMRA_GLM5_MTP=1; a full MoE layer, unloaded by default) \
1998 or the DFlash2 drafter (MEMRA_GLM5_DFLASH=<dir-or-hf-spec>)"
1999 )
2000 })?;
2001 if prompt.len() < 2 {
2002 return Err(
2003 "generate_spec_glm5 needs a prompt of >= 2 tokens (the MTP plane warms on \
2004 (token[i+1], hidden[i]) pairs)"
2005 .into(),
2006 );
2007 }
2008 if dflash_src.is_none() && crate::spec::spec_hpost() {
2009 // MTP-carrier-specific refusal: the DFlash2 source consumes tapped trunk
2010 // features, not the h_seed carrier, so the flag has nothing to flip there.
2011 return Err(
2012 "generate_spec_glm5 has no MEMRA_SPEC_HPOST arm: the flag flips the MTP \
2013 carrier to the post-norm hidden, but this loop seeds every committed pair \
2014 from the trunk's PRE-output_norm collapsed rows (LANE.md §A). Mixing the \
2015 two silently degrades drafts; the HPOST twin needs its own gate before it \
2016 may run"
2017 .into(),
2018 );
2019 }
2020 // ppN split (lane/glm5-ppn-verify): the verify walk, the rollback and the MTP
2021 // chain all run under the split now — but an UNQUALIFIED pipeline rewrite still
2022 // refuses loudly at the session seam, before any cache is allocated over
2023 // stage-sharded weights (worker admission additionally bounds the stage count to
2024 // the gated set; see glm5_spec_capable).
2025 if crate::pp::pp_cuts(self.layers.len()).is_some()
2026 && !self.rewrite_allowed(memra_gguf::execution_manifest::RewriteSurface::Pipeline)
2027 {
2028 return Err("pipeline rewrite is not qualified for this ModelPlan".into());
2029 }
2030 // PENALTY ARM admission (lane/spec-exclusions-20260902): a penalized request opens
2031 // only under MEMRA_SPEC_PENALTY=1; `pen` rides the session for every round, and
2032 // `sampling` keeps its route meaning (`None` = greedy verify) — a greedy request
2033 // with penalties is `sampling: None, pen: Some`.
2034 let pen = glm5_penalty_admit(sampling.as_ref())?;
2035 let sampling = sampling.filter(|sp| sp.temp > 0.0);
2036 // Room for the prompt, the anchor row and at least one verify round.
2037 if prompt.len() + 4 > ctx_cap {
2038 return Err(format!(
2039 "glm5 spec session needs ctx for prompt {} + anchor + one verify round, \
2040 cap {ctx_cap}",
2041 prompt.len()
2042 )
2043 .into());
2044 }
2045 let n_vocab = self.output.out_features();
2046 // FR-SPEC TRIM (module doc): a loaded `MEMRA_FRSPEC_TRIM` artifact means the draft
2047 // head projects over gathered top-N rows and every draft pick is a RANK id that
2048 // must remap through d2t to the true vocabulary BEFORE it is chained or verified.
2049 // The verify walk stays full-vocab regardless — the invariant under gate.
2050 if let Some(map) = self.glm5_d2t() {
2051 if map.iter().any(|&t| t as usize >= n_vocab) {
2052 return Err(format!(
2053 "glm5 FR-Spec d2t carries a token id >= n_vocab {n_vocab} — the ranks \
2054 artifact was minted for a different vocabulary"
2055 )
2056 .into());
2057 }
2058 // Engagement receipt (the dspark trim receipt's shape): the server-log line
2059 // the trim arm's per-session engagement is verified by.
2060 eprintln!("{}", self.glm5_trim_engagement_line(map));
2061 }
2062 // Confidence-gate engagement receipt (loop-port 2; the deploy-gate greps this —
2063 // never-serve-greedy law's receipt discipline): armed iff MEMRA_SPEC_PMIN > 0.
2064 if glm5_pmin() > 0.0 {
2065 eprintln!(
2066 "[glm5-spec] draft confidence gate armed: PMIN={:.3} PMIN0={} (native \
2067 chain p-of-pick; DFlash2 selector-q tau-slot truncation)",
2068 glm5_pmin(),
2069 glm5_pmin0() as u8,
2070 );
2071 }
2072 // The MTP plane index is a NATIVE-arm need; the DFlash2 source never touches the
2073 // plane (it still allocates below — plan-structural, the named cost in the module
2074 // doc — but nothing reads or resets it).
2075 let mtp_il = match dflash_src {
2076 Some(_) => None,
2077 None => Some(
2078 self.plan
2079 .mtp_blocks
2080 .first()
2081 .ok_or("ModelPlan declares no MTP block")?
2082 .layer
2083 .index as usize,
2084 ),
2085 };
2086 // FIRST-TOKEN PROFILE (lane/b200-spec-ttft-20260902, `MEMRA_SPEC_PROF=1`): the
2087 // head engine resolves before any device work (a pure placement lookup), so the
2088 // clock can bound every creation phase on both the primary and head streams.
2089 let eh = self.glm5_head_engine(e)?;
2090 let mut prof = spec_prof_on().then(SpecFirstTokenProf::default);
2091 let mut pclk = prof.as_ref().map(|_| ProfClock::start(e, eh));
2092 if let Some(pf) = prof.as_mut() {
2093 pf.free_mb_before = self.glm5_free_mb(e);
2094 }
2095 // Stage-owned allocation under a split (each layer's planes on its stage's device,
2096 // trailing MTP plane on the last stage); door shut = plain `Cache::new_planned`.
2097 let mut cache = crate::pp::new_cache_planned(e, &self.cfg, &self.plan, ctx_cap)?;
2098 if let (Some(pf), Some(ck)) = (prof.as_mut(), pclk.as_mut()) {
2099 pf.cache_alloc_ms = ck.lap(e, eh);
2100 }
2101
2102 // ---- prime, boundary token, draft-source warm over the prompt ----
2103 // `prime_cache` routes to its own ppN twin under the split; `hiddens` is owned by
2104 // the LAST stage's engine (its published contract) — exactly where the MTP chain
2105 // below runs, so the warm consumes it with no device bounce. DFlash2 source: the
2106 // prime walk fills the armed HcTapSink with every prompt row's contracted tap
2107 // features (the drafter's context; round 1 ingests them into its own KV).
2108 let plen = prompt.len();
2109 let n_embd = self.cfg.n_embd as usize;
2110 let tap_layers = match dflash_src {
2111 Some(dr) => Some(glm5_dflash_tap_layers(&dr.draft, self.layers.len())?),
2112 None => None,
2113 };
2114 // DRAFTER PRIME ARM (lane/spec-route-depth-20260902): the device-resident arm ingests
2115 // taps inside the prime at every range boundary; the eager arm (the pre-lane literal)
2116 // primes once over a whole-prompt host sink and leaves the ingest to round 1. (The
2117 // chunked host-tap arm, MEMRA_GLM5_DRAFT_PRIME_V2, was REJECTED on the pair and removed
2118 // 2026-09-05.) `hidden_rows` is the row count of the returned `hiddens` stack (the whole
2119 // prompt on the eager arm, the LAST chunk on the chunked arm) — the boundary
2120 // capture indexes its last row through it.
2121 let mut v2_kv: Option<DflashKv> = None;
2122 let (logits0, hiddens, hidden_rows) = match (dflash_src, tap_layers.as_ref()) {
2123 (Some(dr), Some(taps)) if glm5_draft_taps_device_on() => {
2124 // DEVICE-RESIDENT ARM (doc on `glm5_draft_taps_device_on`): ONE whole-prompt
2125 // prime; the ingest happens inside it at every range boundary.
2126 let kv = DflashKv::new(eh, &dr.draft.cfg, ctx_cap)?;
2127 if let (Some(pf), Some(ck)) = (prof.as_mut(), pclk.as_mut()) {
2128 pf.draft_alloc_ms = ck.lap(e, eh);
2129 pf.draft_kv_mb = dflash_kv_bytes(&dr.draft.cfg, ctx_cap) as f64 / 1e6;
2130 }
2131 let ring = crate::hybrid_forward::hyper_prime_call_rows(
2132 plen,
2133 self.layers.len(),
2134 self.gdn_prime_grid_on(),
2135 );
2136 let mut sink = HcTapSink::new_device_staged_at(taps.clone(), n_embd, ring, 0);
2137 sink.ingest_state = Some(Box::new(Glm5DraftPrimeInflight {
2138 kv,
2139 taps: taps.clone(),
2140 n_embd,
2141 ring,
2142 stage: (0..taps.len()).map(|_| None).collect(),
2143 rows_dev: None,
2144 prof_on: prof.is_some(),
2145 copy_ms: 0.0,
2146 feat_ms: 0.0,
2147 kv_ms: 0.0,
2148 chunks: 0,
2149 }));
2150 cache.hc_taps = Some(sink);
2151 let (l, _seed, h) = self.prime_cache(e, prompt, &mut cache, 0)?;
2152 let walk_ms = pclk.as_mut().map(|ck| ck.lap(e, eh));
2153 let mut sink = cache
2154 .hc_taps
2155 .take()
2156 .ok_or("device-resident drafter prime: tap sink vanished")?;
2157 let st = sink
2158 .ingest_state
2159 .take()
2160 .ok_or("device-resident drafter prime: ingest state vanished")?
2161 .downcast::<Glm5DraftPrimeInflight>()
2162 .map_err(|_| "device-resident drafter prime: ingest state of the wrong type")?;
2163 let st = *st;
2164 if st.kv.len != plen {
2165 return Err(format!(
2166 "device-resident drafter prime covered {} of {plen} prompt rows \
2167 (the prime's range loop must hand every range to the ingest)",
2168 st.kv.len
2169 )
2170 .into());
2171 }
2172 if let (Some(pf), Some(walk)) = (prof.as_mut(), walk_ms) {
2173 // The ingest ran INSIDE the prime walk: split it back out so `prime`
2174 // stays the trunk's share and `draft_prime` the drafter's.
2175 let ingest = st.copy_ms + st.feat_ms + st.kv_ms;
2176 pf.prime_ms = (walk - ingest).max(0.0);
2177 pf.draft_prime_ms = ingest;
2178 pf.draft_prime_h2d_ms = st.copy_ms;
2179 pf.draft_prime_feat_ms = st.feat_ms;
2180 pf.draft_prime_kv_ms = st.kv_ms;
2181 pf.draft_prime_rows = plen;
2182 pf.draft_prime_chunks = st.chunks;
2183 pf.draft_prime_arm = "device";
2184 }
2185 v2_kv = Some(st.kv);
2186 (l, h, plen)
2187 }
2188 _ => {
2189 if let Some(taps) = tap_layers.as_ref() {
2190 let t_sink = std::time::Instant::now();
2191 cache.hc_taps = Some(HcTapSink::new(taps.clone(), n_embd, plen));
2192 if let Some(pf) = prof.as_mut() {
2193 pf.sink_alloc_ms = t_sink.elapsed().as_secs_f64() * 1e3;
2194 }
2195 }
2196 let (l, _seed, h) = self.prime_cache(e, prompt, &mut cache, 0)?;
2197 if let (Some(pf), Some(ck)) = (prof.as_mut(), pclk.as_mut()) {
2198 pf.prime_ms = ck.lap(e, eh);
2199 pf.prime_tap_dtoh_ms = cache
2200 .hc_taps
2201 .as_ref()
2202 .map(|sk| sk.dtoh_ns as f64 / 1e6)
2203 .unwrap_or(0.0);
2204 }
2205 (l, h, plen)
2206 }
2207 };
2208 // Prompt-boundary capture (lane/glm5-prefix-latent2): taken NOW — after the prime
2209 // filled every plane to the boundary, before the anchor/draft machinery below and
2210 // before any burst mutates the conv/ssm state or laps the tail ring. DFlash2-only:
2211 // the native arm's plane fill moves the MTP latent layer past the boundary before a
2212 // capture could be taken, and restore refuses the native source anyway. A refusal
2213 // drops the capture loudly and the session serves regardless.
2214 let prefix_capture = if glm5_spec_prefix_on() && dflash_src.is_some() {
2215 self.glm5_prefix_boundary_capture(e, eh, &cache, &logits0, &hiddens, plen, hidden_rows)
2216 } else {
2217 None
2218 };
2219 if let (Some(pf), Some(ck)) = (prof.as_mut(), pclk.as_mut()) {
2220 pf.capture_ms = ck.lap(e, eh);
2221 }
2222 // The penalty window spans the SESSION (`pen_window_seed`): the prompt now, every
2223 // committed token from here. Empty with penalties off — the anchor rule below is
2224 // then the pre-lane literal in both regimes.
2225 let pen_hist = glm5_pen_window_seed(pen.as_ref(), prompt);
2226 let mut sctr = 0u32;
2227 let anchor = glm5_anchor(
2228 eh,
2229 &logits0,
2230 sampling.as_ref(),
2231 pen.as_ref(),
2232 &pen_hist,
2233 &mut sctr,
2234 "glm5-prime",
2235 )?;
2236 if let (Some(pf), Some(ck)) = (prof.as_mut(), pclk.as_mut()) {
2237 pf.anchor_ms = ck.lap(e, eh);
2238 }
2239
2240 // Keyed on the KIND the general law returned, not on a second local re-derivation:
2241 // a seam whose answer is recomputed by its consumer is decoration. (`tap_layers` is
2242 // `Some` exactly when `dflash_src` is, and the law returns `Dflash2` exactly then, so
2243 // this is the same program the pre-seam code ran — the `_` arm's refusal is the
2244 // never-taken proof of that rather than a silent fallback.)
2245 let (mut draft, pending) = match (source_kind, dflash_src, tap_layers) {
2246 (crate::spec::DraftSourceKind::Dflash2, Some(dr), Some(taps)) => {
2247 if let Some(kv) = v2_kv.take() {
2248 // Chunked arm: the KV already holds every prompt row (kv.len == plen);
2249 // round 1 finds nothing pending and walks straight to its block forward.
2250 debug_assert_eq!(kv.len, plen, "chunked drafter prime must cover the prompt");
2251 (
2252 Glm5DraftState::Dflash2 {
2253 kv,
2254 pending: Vec::new(),
2255 taps,
2256 },
2257 Vec::new(),
2258 )
2259 } else {
2260 let sink = cache
2261 .hc_taps
2262 .take()
2263 .ok_or("glm5 dflash prime tap sink vanished")?;
2264 // Drafter ctx KV on the HEAD engine (where the drafter weights loaded and
2265 // every round's chain runs); prompt feature rows ride `pending` so round 1
2266 // ingests them through the one chunked path.
2267 let kv = DflashKv::new(eh, &dr.draft.cfg, ctx_cap)?;
2268 if let Some(pf) = prof.as_mut() {
2269 pf.draft_kv_mb = dflash_kv_bytes(&dr.draft.cfg, ctx_cap) as f64 / 1e6;
2270 }
2271 (
2272 Glm5DraftState::Dflash2 {
2273 kv,
2274 pending: sink.rows,
2275 taps,
2276 },
2277 Vec::new(),
2278 )
2279 }
2280 }
2281 (crate::spec::DraftSourceKind::Dflash2, _, _) => {
2282 return Err(
2283 "the draft-source law selected DFlash2 but this session resolved no tap \
2284 layers — a load-path bug, refused instead of silently drafting from the \
2285 MTP plane (a VANISHED tap sink is a different failure, caught by name in \
2286 the Dflash2 arm itself)"
2287 .into(),
2288 );
2289 }
2290 (crate::spec::DraftSourceKind::NativeMtp, _, _) => {
2291 // BATCHED PLANE WARM (loop-port fold-in — the map's #4, the spec.rs
2292 // `mtp_kv_fill_all` pattern re-aimed at the MLA plane): pairs
2293 // (prompt[i+1], h_i) at plane pos i, i in 0..P-1, filled in CHUNKED
2294 // t-parallel passes instead of P-1 sequential full-block forwards. The
2295 // sequential warm ran ~400 tok/s — the measured +2.5 s TTFT per 1k
2296 // prompt tokens, spec-battery flip condition 1 by name. MTP rows are
2297 // INDEPENDENT given the trunk hiddens (no row-to-row recurrence — the
2298 // plane is the only carrier), so the fill is exact in structure; the
2299 // t>1 attention takes the prime-class program, which can only move
2300 // DRAFTS, never output (verify arbitrates; the byte-identity batteries
2301 // stay the proof).
2302 self.glm5_mtp_plane_fill(eh, &prompt[1..], &hiddens, plen - 1, &mut cache)?;
2303 // pending = committed (token, h_seed) pairs not yet fed to the MTP plane.
2304 // The LAST pair's logits are the next round's first draft — the re-warm
2305 // doubles as draft 1.
2306 let pending = vec![(anchor, self.glm5_seed_row(eh, &hiddens, plen, plen - 1)?)];
2307 (Glm5DraftState::NativeMtp, pending)
2308 }
2309 };
2310 if let (Some(pf), Some(ck)) = (prof.as_mut(), pclk.as_mut()) {
2311 pf.draft_alloc_ms += ck.lap(e, eh);
2312 }
2313 // EAGER-ARM INGEST AT CREATION (doc on `glm5_draft_prime_lazy_on`): the prompt's tap
2314 // rows go into the drafter KV NOW, before the session (and its anchor) is handed to
2315 // the worker, unless the lazy seam asks for the round-1 placement. The chunked arm
2316 // arrives here with nothing pending.
2317 if !glm5_draft_prime_lazy_on()
2318 && let (Glm5DraftState::Dflash2 { kv, pending, taps }, Some(dr)) =
2319 (&mut draft, dflash_src)
2320 && !pending.is_empty()
2321 {
2322 let rows = std::mem::take(pending);
2323 let stats = self.glm5_dflash_ingest_rows(
2324 eh,
2325 &dr.draft,
2326 kv,
2327 &rows,
2328 taps.len() * n_embd,
2329 pclk.as_mut(),
2330 )?;
2331 if let Some(pf) = prof.as_mut() {
2332 stats.write(pf, "eager");
2333 }
2334 }
2335 if let Some(pf) = prof.as_mut() {
2336 pf.free_mb_after = self.glm5_free_mb(e);
2337 }
2338 // Composition engagement receipt — printed immediately before the session is
2339 // RETURNED (after every admission law, the d2t vocabulary check, the cache
2340 // allocation and the prompt prime), so a grep for this line counts sessions that
2341 // actually opened; a refusal or a failure anywhere above never logs it (the #82
2342 // review moved it here after finding four fallible steps below its first home).
2343 if tp_sharded {
2344 eprintln!(
2345 "[glm5-spec] spec x TP composition ARMED (MEMRA_GLM5_SPEC_TP=1): verify \
2346 rows ride the TP shards; rollback restores per-rank planes \
2347 performance_claim=false"
2348 );
2349 }
2350 Ok(Glm5SpecSession {
2351 cache,
2352 committed: prompt.to_vec(),
2353 anchor,
2354 anchor_emitted: false,
2355 pending,
2356 draft,
2357 sampling,
2358 pen,
2359 pen_hist,
2360 sctr,
2361 uctr: 0,
2362 rounds: 0,
2363 rank_trimmed_rounds: 0,
2364 done: false,
2365 max_ctx: ctx_cap,
2366 mtp_il,
2367 prefix_capture,
2368 prof_rounds: prof.as_ref().map(|_| SpecRoundsLog::default()),
2369 prof,
2370 })
2371 }
2372
2373 /// Range-begin hook of the device-resident drafter prime (called by the prime's range
2374 /// loop, `hybrid_forward.rs`): anchor the chunk ring at the range's first row so the
2375 /// walk's `base - origin` lands in `[0, ring)`. No-op on every other sink.
2376 pub(crate) fn glm5_taps_range_begin(&self, cache: &mut Cache, start: usize) {
2377 if let Some(sink) = cache.hc_taps.as_mut()
2378 && sink.ingest_state.is_some()
2379 {
2380 sink.origin = start;
2381 }
2382 }
2383
2384 /// Range-done hook of the device-resident drafter prime: the range's tap planes are
2385 /// complete on their writing devices (the range call returned host logits, so the last
2386 /// stage drained, and every earlier stage's rows were consumed by it), so ingest them
2387 /// now. No-op on every other sink; a state of another type is put back untouched.
2388 pub(crate) fn glm5_taps_range_done(
2389 &self,
2390 e: &Engine,
2391 cache: &mut Cache,
2392 start: usize,
2393 end: usize,
2394 ) -> Res<()> {
2395 let Some(sink) = cache.hc_taps.as_mut() else {
2396 return Ok(());
2397 };
2398 let Some(state) = sink.ingest_state.take() else {
2399 return Ok(());
2400 };
2401 let mut st = match state.downcast::<Glm5DraftPrimeInflight>() {
2402 Ok(st) => st,
2403 Err(other) => {
2404 sink.ingest_state = Some(other);
2405 return Ok(());
2406 }
2407 };
2408 let res = self.glm5_taps_ingest_range(e, sink, &mut st, start, end);
2409 sink.ingest_state = Some(st);
2410 res
2411 }
2412
2413 /// One range of the device-resident drafter prime: per tap slot, the plane on its
2414 /// writing device is interleaved into the head-device fc input with a 2D copy (after a
2415 /// peer copy into head-device staging when the slot's stage is another device; the
2416 /// source stream is drained before the plane is read), then `ctx_features` +
2417 /// `ingest_ctx` at the range width append the rows to the drafter KV.
2418 fn glm5_taps_ingest_range(
2419 &self,
2420 e: &Engine,
2421 sink: &mut HcTapSink,
2422 st: &mut Glm5DraftPrimeInflight,
2423 start: usize,
2424 end: usize,
2425 ) -> Res<()> {
2426 let t = end - start;
2427 if t == 0 {
2428 return Ok(());
2429 }
2430 if t > st.ring {
2431 return Err(format!(
2432 "device-resident tap ingest: range {start}..{end} ({t} rows) exceeds the \
2433 sink ring of {} rows",
2434 st.ring
2435 )
2436 .into());
2437 }
2438 if st.kv.len != start {
2439 return Err(format!(
2440 "device-resident tap ingest: drafter KV holds {} rows but the range starts \
2441 at {start} (a range was skipped or handed twice)",
2442 st.kv.len
2443 )
2444 .into());
2445 }
2446 let eh = self.glm5_head_engine(e)?;
2447 let dr = self
2448 .glm5_dflash
2449 .as_ref()
2450 .ok_or("device-resident tap ingest without a loaded drafter")?;
2451 let h = st.n_embd;
2452 let n_taps = st.taps.len();
2453 let mut pclk = st.prof_on.then(|| ProfClock::start(e, eh));
2454 if st.rows_dev.is_none() {
2455 st.rows_dev = Some(eh.uninit(st.ring * n_taps * h)?);
2456 }
2457 let rows_dev = st.rows_dev.as_mut().expect("just filled");
2458 for (slot, &il) in st.taps.iter().enumerate() {
2459 let buf = sink.dev[slot].as_ref().ok_or_else(|| {
2460 format!(
2461 "device-resident tap ingest: slot {slot} (layer {il}) was never written \
2462 over rows {start}..{end}"
2463 )
2464 })?;
2465 let es = self.glm5_tap_slot_engine(e, il)?;
2466 if es.ctx().ordinal() == eh.ctx().ordinal() {
2467 eh.copy_2d_dtod_async(rows_dev, slot * h, n_taps * h, buf, h, h, t)?;
2468 } else {
2469 if st.stage[slot].is_none() {
2470 st.stage[slot] = Some(eh.uninit(st.ring * h)?);
2471 }
2472 let staging = st.stage[slot].as_mut().expect("just filled");
2473 eh.copy_peer_from_async(staging, es, buf, t * h)?;
2474 es.stream().synchronize()?;
2475 eh.copy_2d_dtod_async(rows_dev, slot * h, n_taps * h, staging, h, h, t)?;
2476 }
2477 }
2478 if let Some(ck) = pclk.as_mut() {
2479 st.copy_ms += ck.lap(e, eh);
2480 }
2481 let feats = dr.draft.ctx_features(eh, rows_dev, t)?;
2482 if let Some(ck) = pclk.as_mut() {
2483 st.feat_ms += ck.lap(e, eh);
2484 }
2485 let pos: Vec<i32> = ((st.kv.len as i32)..(st.kv.len + t) as i32).collect();
2486 dr.draft.ingest_ctx(eh, &mut st.kv, &feats, &pos, t)?;
2487 if let Some(ck) = pclk.as_mut() {
2488 st.kv_ms += ck.lap(e, eh);
2489 }
2490 st.chunks += 1;
2491 Ok(())
2492 }
2493
2494 /// The EAGER drafter ingest (one implementation for session creation and round 1):
2495 /// host feature rows `[n, row_w]` -> 256-row pageable HtoD chunks -> `ctx_features` ->
2496 /// `ingest_ctx`, appended at `kv.len`. Returns the profile buckets (all 0 with the
2497 /// clock absent).
2498 fn glm5_dflash_ingest_rows(
2499 &self,
2500 eh: &Engine,
2501 draft: &DflashDraft,
2502 kv: &mut DflashKv,
2503 rows: &[f32],
2504 row_w: usize,
2505 mut pclk: Option<&mut ProfClock>,
2506 ) -> Res<DraftIngestStats> {
2507 debug_assert_eq!(rows.len() % row_w, 0, "ragged feature rows");
2508 let n_new = rows.len() / row_w;
2509 let mut st = DraftIngestStats {
2510 rows: n_new,
2511 ..Default::default()
2512 };
2513 let mut r0 = 0usize;
2514 while r0 < n_new {
2515 let t_c = (n_new - r0).min(256);
2516 let chunk = eh.htod(&rows[r0 * row_w..(r0 + t_c) * row_w])?;
2517 if let Some(ck) = pclk.as_deref_mut() {
2518 st.h2d_ms += ck.lap(eh, eh);
2519 }
2520 let feats = draft.ctx_features(eh, &chunk, t_c)?;
2521 if let Some(ck) = pclk.as_deref_mut() {
2522 st.feat_ms += ck.lap(eh, eh);
2523 }
2524 let pos_c: Vec<i32> = ((kv.len as i32)..(kv.len + t_c) as i32).collect();
2525 draft.ingest_ctx(eh, kv, &feats, &pos_c, t_c)?;
2526 if let Some(ck) = pclk.as_deref_mut() {
2527 st.kv_ms += ck.lap(eh, eh);
2528 }
2529 r0 += t_c;
2530 st.chunks += 1;
2531 }
2532 Ok(st)
2533 }
2534
2535 /// Free device memory per device this model can hold state on (primary + ppN stages),
2536 /// for the depth profile's before/after samples.
2537 fn glm5_free_mb(&self, e: &Engine) -> Vec<(usize, u64)> {
2538 let mut out = vec![(e.ctx().ordinal(), e.free_mem_mb())];
2539 if let Ok(rt) = crate::pp::PpNRt::get(e) {
2540 for stage in 0..rt.n_stages() {
2541 let se = rt.engine(stage, e);
2542 let ord = se.ctx().ordinal();
2543 if !out.iter().any(|(o, _)| *o == ord) {
2544 out.push((ord, se.free_mem_mb()));
2545 }
2546 }
2547 }
2548 out
2549 }
2550
2551 /// The engine that OWNS tap layer `il`'s device (the stage engine under a live split,
2552 /// the caller's engine otherwise) — the `glm5_tap_drain` placement rule, shared.
2553 fn glm5_tap_slot_engine<'e>(&self, e: &'e Engine, il: usize) -> Res<&'e Engine> {
2554 match crate::pp::pp_cuts(self.layers.len()) {
2555 Some(fence) if !crate::pp::pp2_streams_off() => {
2556 let rt = crate::pp::PpNRt::get(e)?;
2557 let stage = fence
2558 .windows(2)
2559 .position(|w| il >= w[0] && il < w[1])
2560 .ok_or_else(|| format!("tap layer {il} outside every stage range"))?;
2561 Ok(rt.engine(stage, e))
2562 }
2563 _ => Ok(e),
2564 }
2565 }
2566
2567 /// Boundary capture for the DEFERRED prefix publication (lane/glm5-prefix-latent2,
2568 /// 2026-09-01): the generation-destroyed state at `pos == plen` — conv/ssm via
2569 /// `Cache::snapshot` (D2D copies), per-layer latent tails via `snapshot_tail`, the
2570 /// prime's boundary logits, the pre-output_norm boundary hidden. `None` (loud) on any
2571 /// refusal — a capture is an optimization the session must never fail on. The
2572 /// append-only planes (latent rows, final pool keys, full-attn KV) are NOT copied here:
2573 /// the worker slices them from the live cache at publish (`snapshot_plane_at`), which is
2574 /// legal because the glm5 verify rollback never truncates below the prime boundary.
2575 #[allow(clippy::too_many_arguments)]
2576 // allow: the parameter list is the capture contract (two engines, the cache, the
2577 // boundary logits and hidden stack, the boundary and the stack's own row count);
2578 // bundling would hide which row index is which
2579 fn glm5_prefix_boundary_capture(
2580 &self,
2581 e: &Engine,
2582 eh: &Engine,
2583 cache: &Cache,
2584 logits0: &[f32],
2585 hiddens: &CudaSlice<f32>,
2586 plen: usize,
2587 // Rows in `hiddens` (its last row is the boundary hidden): the whole prompt on the
2588 // eager cold prime, the last chunk on the chunked arm, the suffix on a restore.
2589 hidden_rows: usize,
2590 ) -> Option<crate::spec::SpecBoundaryCapture> {
2591 debug_assert_eq!(
2592 cache.pos, plen,
2593 "boundary capture must sit at the prime boundary"
2594 );
2595 let snap = match cache.snapshot(e) {
2596 Ok(s) => s,
2597 Err(err) => {
2598 eprintln!("[glm5-spec] prefix boundary capture SKIPPED (cache snapshot: {err})");
2599 return None;
2600 }
2601 };
2602 // The ONLY latent layer that may legitimately sit empty at the boundary is the
2603 // MTP/NextN plane (allocated, never executed by the trunk on the DFlash2 arm).
2604 // Identity-keyed, not length-keyed (PR #96 review round 2, finding 1): a TRUNK
2605 // plane empty at the boundary is a regression that must refuse the capture, or
2606 // three length-keyed layers downstream would each wave it through and reproduce
2607 // the parent lane's fabrication shape per-layer.
2608 let mtp_plane_il = self.plan.mtp_blocks.first().map(|b| b.layer.index as usize);
2609 let mut latent_tails = Vec::with_capacity(cache.latent.len());
2610 for (il, l) in cache.latent.iter().enumerate() {
2611 match l {
2612 Some(l) if l.len == 0 && cache.pos > 0 => {
2613 if Some(il) != mtp_plane_il {
2614 eprintln!(
2615 "[glm5-spec] prefix boundary capture SKIPPED (trunk latent \
2616 layer {il} is EMPTY at the boundary — not the MTP plane; a \
2617 capture would publish an absent history for a live layer)"
2618 );
2619 return None;
2620 }
2621 latent_tails.push(None)
2622 }
2623 Some(l) => {
2624 if l.len != plen {
2625 eprintln!(
2626 "[glm5-spec] prefix boundary capture SKIPPED (latent layer {il} \
2627 len {} != boundary {plen})",
2628 l.len,
2629 );
2630 return None;
2631 }
2632 match l.snapshot_tail(e) {
2633 Ok(t) => latent_tails.push(Some(t)),
2634 Err(err) => {
2635 eprintln!(
2636 "[glm5-spec] prefix boundary capture SKIPPED (latent layer \
2637 {il}: {err})"
2638 );
2639 return None;
2640 }
2641 }
2642 }
2643 None => latent_tails.push(None),
2644 }
2645 }
2646 let last_h = crate::spec::capture_boundary_hidden(
2647 eh,
2648 hiddens,
2649 hidden_rows,
2650 self.cfg.n_embd as usize,
2651 );
2652 Some(crate::spec::SpecBoundaryCapture {
2653 snap,
2654 pos: plen,
2655 logits: logits0.to_vec(),
2656 last_h,
2657 latent_tails,
2658 })
2659 }
2660
2661 /// Re-arm a glm5 spec session from a RESTORED trunk cache plus a published DFlash2
2662 /// drafter tail — the glm5 twin of `dspark_spec_session_from_restored`, EXTENDED with
2663 /// the suffix prime the multi-turn shape needs (lane/glm5-prefix-latent2, 2026-09-01).
2664 ///
2665 /// WHY IT IS EQUIVALENT TO A COLD PRIME, field by field:
2666 /// * `cache` — the caller's whole-entry restored trunk cache at `fed.len()` (KDA
2667 /// conv/ssm + MLA latent rows + kpool keys + tail ring, the parent lane's restore),
2668 /// and the SUFFIX primes onto it through `prime_cache` — the same continuation
2669 /// program every chunk after the first of a cold prime runs.
2670 /// * drafter — `dkv` is rebuilt from the entry's tail into the SAME absolute rows
2671 /// (`DflashKv::from_tail`, caller-side while the prefix cache is borrowable), and the
2672 /// suffix's tap rows ride `pending` exactly as a cold session's prompt rows do — the
2673 /// drafter's context is the committed tokens either way (a truncated tail below the
2674 /// window can only move ACCEPTANCE, never output: verify arbitrates).
2675 /// * anchor — drawn from the SUFFIX prime's boundary logits with the request's own
2676 /// sampler, exactly the cold composition; Philox counters fresh (a restore is a NEW
2677 /// session — the frspec continuity law, the dspark restore's own convention).
2678 /// * republish — with `glm5_spec_prefix_on()` the session takes a NEW boundary capture
2679 /// at `fed + suffix`, so the next turn hits a DEEPER prefix (the
2680 /// MEMRA_SPEC_RESTORE_REPUBLISH posture; the worker's has_key dedupe drops equals).
2681 ///
2682 /// Refuses (never asserts) whenever the restored halves disagree — a caller that gets
2683 /// `Err` serves the plain hit (correct, slower).
2684 #[allow(clippy::too_many_arguments)]
2685 pub fn glm5_spec_session_from_restored(
2686 &self,
2687 e: &Engine,
2688 mut cache: Cache,
2689 fed: &[u32],
2690 suffix: &[u32],
2691 // The ENTRY's boundary logits (`ReuseEntry::last_logits`), read ONLY on the
2692 // full-cover arm (`suffix.is_empty()`), where there is no suffix prime to draw the
2693 // anchor from. A suffix-bearing restore ignores it and uses the prime's own row.
2694 boundary_logits: &[f32],
2695 dkv: DflashKv,
2696 ctx_cap: usize,
2697 sampling: Option<SpecSampling>,
2698 ) -> Res<Glm5SpecSession> {
2699 if self.hyper.is_none() {
2700 return Err("glm5_spec_session_from_restored requires a HyperConnections trunk".into());
2701 }
2702 // The composition laws a cold session enforces hold here too, fail-closed and by
2703 // name — a restored session must never be the door around an admission law.
2704 let tp_sharded = self.layers.iter().any(|l| match &l.mixer {
2705 Mixer::Kda(la) => la.tp.is_some(),
2706 Mixer::Mla(mla) => mla.tp.is_some(),
2707 _ => false,
2708 });
2709 if tp_sharded {
2710 return Err(
2711 "restored glm5 spec sessions carry no TP arm (the spec x TP composition is \
2712 cold-session gated only); the plain hit serves"
2713 .into(),
2714 );
2715 }
2716 if crate::pp::pp_cuts(self.layers.len()).is_some()
2717 && !self.rewrite_allowed(memra_gguf::execution_manifest::RewriteSurface::Pipeline)
2718 {
2719 return Err("pipeline rewrite is not qualified for this ModelPlan".into());
2720 }
2721 // DFlash2 source ONLY: the native MTP plane fill consumes trunk hiddens the restored
2722 // range does not have (re-running the trunk over it would be a second prime — the
2723 // whole cost this restore exists to avoid).
2724 let dr = self.glm5_dflash.as_ref().ok_or(
2725 "restored glm5 spec sessions require the DFlash2 drafter (MEMRA_GLM5_DFLASH): \
2726 the native MTP plane cannot be re-warmed from restored KV",
2727 )?;
2728 let source = crate::spec::resolve_draft_source_kind(
2729 self.plan.draft_source,
2730 self.mtp.is_some(),
2731 true,
2732 )
2733 .map_err(|why| format!("restored glm5 spec session has no draft source ({why})"))?;
2734 if !matches!(source, crate::spec::DraftSourceKind::Dflash2) {
2735 return Err(
2736 "restored glm5 spec sessions require the DFlash2 draft source; the plan \
2737 selected another"
2738 .into(),
2739 );
2740 }
2741 // PENALTY ARM admission: the cold constructor's law, verbatim (doc there).
2742 let pen = glm5_penalty_admit(sampling.as_ref())?;
2743 let sampling = sampling.filter(|sp| sp.temp > 0.0);
2744 if fed.is_empty() {
2745 return Err("restored glm5 spec session needs a non-empty restored prefix".into());
2746 }
2747 // FULL-COVER ARM (memra#74, lane/glm5-fullcover-spec-route): an empty suffix is the
2748 // repeated-prompt shape. It refuses unless the arm is armed AND the entry carried
2749 // its boundary logits: without them there is no anchor row and no way to start a
2750 // round, which is exactly the `spec_restore_refusal` full-cover rule for MTP.
2751 if suffix.is_empty() {
2752 if !glm5_spec_fullcover_on() {
2753 return Err(
2754 "restored glm5 spec session needs a non-empty suffix unless \
2755 MEMRA_GLM5_SPEC_FULLCOVER=1 (empty-suffix full-cover hits otherwise \
2756 keep the plain boundary-logits resume)"
2757 .into(),
2758 );
2759 }
2760 if boundary_logits.len() != self.output.out_features() {
2761 return Err(format!(
2762 "full-cover glm5 spec restore needs the entry's boundary logits \
2763 ({} rows, got {})",
2764 self.output.out_features(),
2765 boundary_logits.len(),
2766 )
2767 .into());
2768 }
2769 }
2770 if cache.pos != fed.len() {
2771 return Err(format!(
2772 "restored glm5 spec session needs a whole-entry trunk cache: cache.pos {} \
2773 != restored prefix {}",
2774 cache.pos,
2775 fed.len(),
2776 )
2777 .into());
2778 }
2779 if dkv.len != fed.len() {
2780 return Err(format!(
2781 "restored draft KV len {} != restored prefix {}",
2782 dkv.len,
2783 fed.len(),
2784 )
2785 .into());
2786 }
2787 if dkv.cap != ctx_cap {
2788 return Err(
2789 format!("restored draft KV cap {} != session ctx {ctx_cap}", dkv.cap,).into(),
2790 );
2791 }
2792 // Room for prefix + suffix, the anchor row and at least one verify round.
2793 if fed.len() + suffix.len() + 4 > ctx_cap {
2794 return Err(format!(
2795 "restored glm5 spec session needs ctx for prefix {} + suffix {} + anchor + \
2796 one verify round, cap {ctx_cap}",
2797 fed.len(),
2798 suffix.len(),
2799 )
2800 .into());
2801 }
2802 let n_vocab = self.output.out_features();
2803 if let Some(map) = self.glm5_d2t() {
2804 if map.iter().any(|&t| t as usize >= n_vocab) {
2805 return Err(format!(
2806 "glm5 FR-Spec d2t carries a token id >= n_vocab {n_vocab} — the ranks \
2807 artifact was minted for a different vocabulary"
2808 )
2809 .into());
2810 }
2811 eprintln!("{}", self.glm5_trim_engagement_line(map));
2812 }
2813 if glm5_pmin() > 0.0 {
2814 eprintln!(
2815 "[glm5-spec] draft confidence gate armed: PMIN={:.3} PMIN0={} (native \
2816 chain p-of-pick; DFlash2 selector-q tau-slot truncation)",
2817 glm5_pmin(),
2818 glm5_pmin0() as u8,
2819 );
2820 }
2821 // ---- suffix prime over the restored planes (the continuation program), taps armed
2822 // for exactly the suffix rows (`HcTapSink::origin` anchors the sink at the restored
2823 // boundary; the chunked walk's absolute bases rebase through it).
2824 let n_embd = self.cfg.n_embd as usize;
2825 let taps = glm5_dflash_tap_layers(&dr.draft, self.layers.len())?;
2826 let eh = self.glm5_head_engine(e)?;
2827 // ---- STREAM ORDERING, UNCONDITIONAL AND BEFORE THE SUFFIX BRANCH (memra#95, the
2828 // fleet-fatal full-cover panic).
2829 //
2830 // Everything this session restores was written on the CALLER's stream, in the
2831 // worker's admission path: the trunk planes by `prefix_restore_at`, and the DFlash2
2832 // drafter ctx KV by `DflashKv::from_tail` (worker.rs, the "glm5 spec restore, half 1
2833 // of 2" block — a fresh allocation plus per-layer `memset_zeros_view` +
2834 // `copy_range_into`). The draft phase of round 1 then READS that KV through
2835 // `eh` = `glm5_head_engine`, which under a live ppN split is the last stage's OWN
2836 // Engine (`pp::PpNRt::build` gives every stage s>0 its own Engine even on the primary
2837 // device, for scratch-pool isolation) and is used OUTSIDE any `rt.enter` scope — so
2838 // it launches on that Engine's own stream, which is neither the caller's stream nor
2839 // the stage stream. Nothing ordered the two, and the drafter's first `forward_round`
2840 // could read the ctx rows before the import landed.
2841 //
2842 // `fence_stages_behind` is NOT the fix and was the first thing this lane got wrong:
2843 // it orders `StageRt::stream`, the enter-scope stream, and says nothing about a stage
2844 // ENGINE's own stream. `order_engine_behind` is the seam for a body that hands a
2845 // stage engine to a helper without entering the stage.
2846 //
2847 // Why only the FULL-COVER arm: the suffix arm calls `prime_cache` on `e`, whose
2848 // logits come back through `Engine::dtoh` (a `stream().synchronize()`), which drains
2849 // the caller's stream before round 1 ever runs. The full-cover arm has no prime and
2850 // no device readback at all between the import and the round — the anchor is drawn
2851 // from the entry's host-side boundary logits — so it is the only restored path with
2852 // nothing between the two. That is why production never saw this on the suffix path,
2853 // and it is why the ordering is done HERE, once per session and for both arms,
2854 // instead of relying on a host sync that belongs to the prime.
2855 //
2856 // The measured failure: a NaN drafter row -> the top-k selector's documented
2857 // exhausted-slot sentinel (`0xffffffff`, `cu/kernels.cu` `topk_rows_f32` and its
2858 // sharded twin) -> `walk: candidate 4294967295 outside codebook vocab` at
2859 // dflash.rs:221/:804, on the FIRST round of a restored session (the receipts put the
2860 // panic between the `RESTORED session` line and the round's first `[glm5-acc]`),
2861 // fleet-fatal after one respawn.
2862 crate::pp::PpNRt::order_engine_behind(e, eh)?;
2863 // FIRST-TOKEN PROFILE (`MEMRA_SPEC_PROF=1`): the restored shape pays no cache or
2864 // drafter allocation here (the caller restored both); its prime bucket is the
2865 // SUFFIX prime (0 on a full-cover hit), which is what makes the restore worth having.
2866 let mut prof = spec_prof_on().then(SpecFirstTokenProf::default);
2867 let mut pclk = prof.as_ref().map(|_| ProfClock::start(e, eh));
2868 // FULL COVER: no suffix, so no prime, no taps and no republish (the entry ALREADY
2869 // sits at this boundary, and a capture here would be the same key the worker's has_key
2870 // dedupe drops). The boundary row is the entry's own; the drafter ctx KV is already
2871 // at `cache.pos` from the tail, so `pending` is empty and round 1's
2872 // `kv.len == cache.pos` invariant holds without an ingest.
2873 let (logits_s, tap_rows, prefix_capture) = if suffix.is_empty() {
2874 (boundary_logits.to_vec(), Vec::new(), None)
2875 } else {
2876 cache.hc_taps = Some(HcTapSink::new_at(
2877 taps.clone(),
2878 n_embd,
2879 suffix.len(),
2880 fed.len(),
2881 ));
2882 let (logits_s, _seed, hiddens) = self.prime_cache(e, suffix, &mut cache, 0)?;
2883 if let (Some(pf), Some(ck)) = (prof.as_mut(), pclk.as_mut()) {
2884 pf.prime_ms = ck.lap(e, eh);
2885 }
2886 // Republish capture at the NEW (deeper) boundary — pos == fed + suffix here.
2887 let capture = if glm5_spec_prefix_on() {
2888 // `hiddens` is the SUFFIX stack: its last row is the boundary hidden
2889 // (indexed through `suffix.len()`, not the absolute boundary).
2890 self.glm5_prefix_boundary_capture(
2891 e,
2892 eh,
2893 &cache,
2894 &logits_s,
2895 &hiddens,
2896 cache.pos,
2897 suffix.len(),
2898 )
2899 } else {
2900 None
2901 };
2902 let sink = cache
2903 .hc_taps
2904 .take()
2905 .ok_or("glm5 restored-session suffix tap sink vanished")?;
2906 (logits_s, sink.rows, capture)
2907 };
2908 if let (Some(pf), Some(ck)) = (prof.as_mut(), pclk.as_mut()) {
2909 pf.capture_ms = ck.lap(e, eh);
2910 }
2911 let mut committed = Vec::with_capacity(fed.len() + suffix.len());
2912 committed.extend_from_slice(fed);
2913 committed.extend_from_slice(suffix);
2914 // The penalty window is the whole committed prompt (restored prefix + suffix), which
2915 // is what the plain hit's sampler replays too ("penalty history replayed over the
2916 // whole prefix"). Empty with penalties off.
2917 let pen_hist = glm5_pen_window_seed(pen.as_ref(), &committed);
2918 let mut sctr = 0u32;
2919 let anchor = glm5_anchor(
2920 eh,
2921 &logits_s,
2922 sampling.as_ref(),
2923 pen.as_ref(),
2924 &pen_hist,
2925 &mut sctr,
2926 "glm5-restore",
2927 )?;
2928 if let (Some(pf), Some(ck)) = (prof.as_mut(), pclk.as_mut()) {
2929 pf.anchor_ms = ck.lap(e, eh);
2930 }
2931 // Engagement receipt (the dspark restore's shape — the deploy gate greps this; a
2932 // cached_tokens number alone cannot distinguish a spec restore from a plain hit).
2933 eprintln!(
2934 "[glm5-spec] RESTORED session: {} prefix tokens + {} suffix from cache — no \
2935 cold prime (drafter tail rows {}, arm {})",
2936 fed.len(),
2937 suffix.len(),
2938 dkv.len,
2939 if suffix.is_empty() {
2940 "full-cover"
2941 } else {
2942 "suffix-prime"
2943 },
2944 );
2945 Ok(Glm5SpecSession {
2946 cache,
2947 committed,
2948 anchor,
2949 anchor_emitted: false,
2950 pending: Vec::new(),
2951 draft: Glm5DraftState::Dflash2 {
2952 kv: dkv,
2953 pending: tap_rows,
2954 taps,
2955 },
2956 sampling,
2957 pen,
2958 pen_hist,
2959 sctr,
2960 uctr: 0,
2961 rounds: 0,
2962 rank_trimmed_rounds: 0,
2963 done: false,
2964 max_ctx: ctx_cap,
2965 mtp_il: None,
2966 prefix_capture,
2967 prof_rounds: prof.as_ref().map(|_| SpecRoundsLog::default()),
2968 prof,
2969 })
2970 }
2971
2972 /// The loaded FR-Spec draft->target map for the session's DRAFT SOURCE (None = full-vocab
2973 /// head, rank id == token id). A loaded DFlash2 drafter wins the source selection, so its
2974 /// trim ([`Self::glm5_dflash_trim`]) is the map; otherwise the embedded head's own map.
2975 fn glm5_d2t(&self) -> Option<&[u32]> {
2976 if self.glm5_dflash.is_some() {
2977 return self.glm5_dflash_trim().map(|(_, d2t)| d2t);
2978 }
2979 self.mtp
2980 .as_ref()
2981 .and_then(|head| head.d2t.as_deref())
2982 .filter(|map| !map.is_empty())
2983 }
2984
2985 /// The DFlash2 round's trimmed draft head `(rows, d2t)`, in the round's preference order:
2986 /// the MtpHead self-trim when its rows came from the TARGET head (an external student's
2987 /// head is never borrowed), else the `dflash_trim` slab (the MEMRA_MTP_SKIP stub, or the
2988 /// glm5 DFlash2 slab built when the NextN block is not loaded, lane/frspec-dflash2-
2989 /// 20260902). None = the full trunk head, stated in the boot receipt.
2990 pub fn glm5_dflash_trim(&self) -> Option<(&crate::model::GpuTensor, &[u32])> {
2991 self.mtp
2992 .as_ref()
2993 .filter(|m| m.d2t_from_target_head)
2994 .and_then(|m| m.shared_head_head.as_ref().zip(m.d2t.as_deref()))
2995 .or_else(|| {
2996 self.dflash_trim
2997 .as_ref()
2998 .map(|t| (&t.head, t.d2t.as_slice()))
2999 })
3000 .filter(|(_, d2t)| !d2t.is_empty())
3001 }
3002
3003 /// The trim engagement receipt at session admission (the dspark trim receipt's shape;
3004 /// the deploy gate greps the server log for it). Source-keyed wording: the DFlash2 route
3005 /// names the slab and the ranks file (`RANK-TRIMMED n_ranks=<n> src=<sha16>`), the native
3006 /// chain keeps its line.
3007 fn glm5_trim_engagement_line(&self, map: &[u32]) -> String {
3008 if self.glm5_dflash.is_some() {
3009 format!(
3010 "[glm5-spec] draft head RANK-TRIMMED n_ranks={} src={}",
3011 map.len(),
3012 self.frspec_src_sha16.as_deref().unwrap_or("unknown")
3013 )
3014 } else {
3015 format!(
3016 "[glm5-spec] draft head TRIMMED to {} rows (FR-Spec d2t engaged)",
3017 map.len()
3018 )
3019 }
3020 }
3021
3022 /// ONE serve burst (the worker's per-tick call, `step_glm5_spec`): rounds of
3023 /// draft(K) -> `glm5_verify_rows` -> accept -> rollback/commit until `target` new
3024 /// tokens are out, EOS commits, or the context guard trips. Returns
3025 /// `(burst, drafted, accepted)`; the burst may overshoot `target` by up to K (a
3026 /// round commits j+1 tokens atomically — the engine surplus stays committed in the
3027 /// session cache and the WORKER clamps public emission to the request budget, the
3028 /// SpecSession overshoot contract).
3029 pub fn glm5_spec_session_burst(
3030 &self,
3031 e: &Engine,
3032 sess: &mut Glm5SpecSession,
3033 target: usize,
3034 k: usize,
3035 eos: &[u32],
3036 ) -> Res<(Vec<u32>, usize, usize)> {
3037 self.glm5_spec_session_burst_inner(
3038 e,
3039 sess,
3040 target,
3041 k,
3042 eos,
3043 &mut Glm5SpecKnobs::default(),
3044 None,
3045 )
3046 }
3047
3048 /// [`glm5_spec_session_burst`] with a ROUND-CADENCE commit hook (lane/b200-spec-ttft-
3049 /// 20260902, the `MEMRA_SPEC_FIRST_TOKEN_EAGER` door's engine half; the spec.rs
3050 /// `on_commit` sse-cadence pattern): `on_commit` is called with every newly committed
3051 /// slice of the burst — first the prime's anchor alone, then each round's `j` accepted
3052 /// drafts + bonus — as DISJOINT, IN-ORDER slices whose concatenation IS the returned
3053 /// burst, byte for byte. The hook returns nothing and the loop's control flow never
3054 /// reads it, so the tokens produced are exactly `glm5_spec_session_burst`'s; only WHEN
3055 /// the caller learns them moves (from burst end to round end). Without a hook the
3056 /// first token of a cold session waits for the whole `target`-token burst.
3057 pub fn glm5_spec_session_burst_streamed(
3058 &self,
3059 e: &Engine,
3060 sess: &mut Glm5SpecSession,
3061 target: usize,
3062 k: usize,
3063 eos: &[u32],
3064 on_commit: CommitHook<'_>,
3065 ) -> Res<(Vec<u32>, usize, usize)> {
3066 self.glm5_spec_session_burst_inner(
3067 e,
3068 sess,
3069 target,
3070 k,
3071 eos,
3072 &mut Glm5SpecKnobs::default(),
3073 Some(on_commit),
3074 )
3075 }
3076
3077 /// [`glm5_spec_session_burst`] with GATE INSTRUMENTS (`Glm5SpecKnobs` — never a serving
3078 /// surface; no serving path constructs a non-default value).
3079 pub fn glm5_spec_session_burst_gated(
3080 &self,
3081 e: &Engine,
3082 sess: &mut Glm5SpecSession,
3083 target: usize,
3084 k: usize,
3085 eos: &[u32],
3086 knobs: &mut Glm5SpecKnobs<'_>,
3087 ) -> Res<(Vec<u32>, usize, usize)> {
3088 self.glm5_spec_session_burst_inner(e, sess, target, k, eos, knobs, None)
3089 }
3090
3091 /// The one burst loop behind the three public entries (plain / streamed / gated).
3092 #[allow(clippy::too_many_arguments)]
3093 // allow: the parameter list is the burst contract plus the two instruments (gate
3094 // knobs, commit hook); bundling would hide which inputs are serving vs instrument
3095 fn glm5_spec_session_burst_inner(
3096 &self,
3097 e: &Engine,
3098 sess: &mut Glm5SpecSession,
3099 target: usize,
3100 k: usize,
3101 eos: &[u32],
3102 knobs: &mut Glm5SpecKnobs<'_>,
3103 mut on_commit: Option<CommitHook<'_>>,
3104 ) -> Res<(Vec<u32>, usize, usize)> {
3105 let cap = Self::hyper_batch_cap();
3106 if k == 0 || k + 1 > cap {
3107 return Err(format!(
3108 "glm5_spec_session_burst: k={k} outside 1..={} (verify rows = k+1 must stay \
3109 inside the decode-exact knee, cap {cap})",
3110 cap - 1
3111 )
3112 .into());
3113 }
3114 if let Glm5DraftState::Dflash2 { .. } = sess.draft {
3115 let b = self
3116 .glm5_dflash
3117 .as_ref()
3118 .ok_or("dflash session on a model with no loaded drafter")?
3119 .draft
3120 .cfg
3121 .block_size;
3122 if k + 1 > b {
3123 return Err(format!(
3124 "glm5_spec_session_burst: k={k} exceeds the DFlash2 drafter's block \
3125 (block_size {b} = anchor + {} drafts, the trained mask pattern) — \
3126 the worker clamps operator K pins to {} for this source; refusing \
3127 loudly rather than drafting an untrained shape",
3128 b - 1,
3129 b - 1
3130 )
3131 .into());
3132 }
3133 }
3134 let d2t = self.glm5_d2t();
3135 if d2t.is_some() && knobs.skip_d2t_remap {
3136 eprintln!("[glm5-spec] d2t REMAP SKIPPED — red-arm instrument, drafts are rank ids");
3137 }
3138 let sp_on: Option<SpecSampling> = sess.sampling.filter(|sp| sp.temp > 0.0);
3139 let mut out: Vec<u32> = Vec::with_capacity(target + k);
3140 let mut drafted = 0usize;
3141 let mut accepted = 0usize;
3142 let mut phase: Option<SpecPhaseNs> =
3143 crate::spec_phase::spec_trace_on().then(SpecPhaseNs::default);
3144 // FIRST-TOKEN PROFILE: the first burst's wall (its tokens are host-visible at
3145 // return, so no drain is needed to bound it).
3146 let first_burst = (!sess.anchor_emitted && sess.prof.is_some())
3147 .then(|| (std::time::Instant::now(), sess.rounds));
3148 // The hook's own share of the first burst's wall (host-only detext + sends), so
3149 // the profile can separate engine time from emission time under the eager door.
3150 let mut hook_ns: u64 = 0;
3151 // sse-cadence flush cursor: everything in out[..flushed] has been handed to on_commit.
3152 let mut flushed = 0usize;
3153 if !sess.anchor_emitted {
3154 // The prime's boundary token: emitted exactly once, by the first burst.
3155 out.push(sess.anchor);
3156 sess.anchor_emitted = true;
3157 if eos.contains(&sess.anchor) {
3158 sess.done = true;
3159 }
3160 if let Some(cb) = on_commit.as_mut() {
3161 let t_cb = first_burst.map(|_| std::time::Instant::now());
3162 cb(&out[flushed..]);
3163 if let Some(t_cb) = t_cb {
3164 hook_ns += t_cb.elapsed().as_nanos() as u64;
3165 }
3166 flushed = out.len();
3167 }
3168 }
3169 while out.len() < target && !sess.done {
3170 // Context guard: a round appends up to k+1 trunk rows from `cache.pos` (and the
3171 // draft plane stays <= pos + k), so the next round must fit with one row slack.
3172 if sess.cache.pos + k + 2 > sess.max_ctx {
3173 sess.done = true;
3174 break;
3175 }
3176 // DEPTH LOG (lane/spec-route-depth-20260902, `MEMRA_SPEC_PROF=1`): the first
3177 // SPEC_PROF_ROUNDS rounds of a session get their own phase accumulator (the
3178 // trace's drains), a wall clock, and the per-row-arm row count — folded into
3179 // the per-burst trace accumulator afterwards so both instruments agree.
3180 let log_round = sess.prof_rounds.as_ref().is_some_and(|l| l.wants_more());
3181 let mut rp = log_round.then(SpecPhaseNs::default);
3182 let t_round = log_round.then(std::time::Instant::now);
3183 let seq0 = V_SEQ_ROWS.load(std::sync::atomic::Ordering::Relaxed);
3184 let ctx0 = sess.cache.pos;
3185 let (round_tokens, n_drafted) = {
3186 let ph: Option<&mut SpecPhaseNs> = match (rp.as_mut(), phase.as_mut()) {
3187 (Some(r), _) => Some(r),
3188 (None, p) => p,
3189 };
3190 self.glm5_spec_round(e, sess, k, d2t, sp_on.as_ref(), knobs, ph)?
3191 };
3192 if let (Some(r), Some(t0)) = (rp.as_ref(), t_round) {
3193 if let Some(p) = phase.as_mut() {
3194 p.add(r);
3195 }
3196 if let Some(log) = sess.prof_rounds.as_mut() {
3197 let ms = |ns: u64| ns as f32 / 1e6;
3198 log.push(SpecRoundProf {
3199 wall_ms: t0.elapsed().as_secs_f32() * 1e3,
3200 draft_ms: ms(r.draft),
3201 verify_ms: ms(r.verify),
3202 accept_ms: ms(r.accept),
3203 rest_ms: ms(r.roll + r.maint),
3204 k: n_drafted as u16,
3205 j: (round_tokens.len() - 1) as u16,
3206 ctx: ctx0 as u32,
3207 seq_rows: (V_SEQ_ROWS.load(std::sync::atomic::Ordering::Relaxed) - seq0)
3208 as u32,
3209 });
3210 }
3211 }
3212 drafted += n_drafted;
3213 accepted += round_tokens.len() - 1; // j accepted drafts + the bonus row
3214 for &tok in &round_tokens {
3215 if eos.contains(&tok) {
3216 sess.done = true;
3217 }
3218 }
3219 out.extend_from_slice(&round_tokens);
3220 sess.rounds += 1;
3221 if let Some(cb) = on_commit.as_mut() {
3222 // sse-cadence: this round's accepted drafts + bonus are committed — hand
3223 // the caller exactly the not-yet-flushed tail (disjoint, in order).
3224 let t_cb = first_burst.map(|_| std::time::Instant::now());
3225 cb(&out[flushed..]);
3226 if let Some(t_cb) = t_cb {
3227 hook_ns += t_cb.elapsed().as_nanos() as u64;
3228 }
3229 flushed = out.len();
3230 }
3231 }
3232 debug_assert!(
3233 on_commit.is_none() || flushed == out.len(),
3234 "every committed token must have been handed to on_commit"
3235 );
3236 if let Some(ph) = phase.as_ref() {
3237 ph.emit("glm5-phase", "glm5-phase-v", k);
3238 }
3239 if let (Some((t0, rounds0)), Some(pf)) = (first_burst, sess.prof.as_mut()) {
3240 pf.first_burst_ms = t0.elapsed().as_secs_f64() * 1e3;
3241 pf.first_burst_hook_ms = hook_ns as f64 / 1e6;
3242 pf.first_burst_rounds = sess.rounds - rounds0;
3243 pf.first_burst_tokens = out.len();
3244 }
3245 Ok((out, drafted, accepted))
3246 }
3247
3248 /// One draft->verify->accept->rollback->re-seed round over the session state. Returns
3249 /// `(round_tokens, n_drafted)`: the round's committed tokens (`j` accepted drafts + the
3250 /// bonus token) and how many drafts actually entered the verify (== `k` today; the
3251 /// confidence gate may truncate it below `k`).
3252 #[allow(clippy::too_many_arguments)]
3253 // allow: the parameter list mirrors the round contract (session + policy + gate knobs +
3254 // the trace accumulator); bundling would hide which inputs are serving vs instrument
3255 fn glm5_spec_round(
3256 &self,
3257 e: &Engine,
3258 sess: &mut Glm5SpecSession,
3259 k: usize,
3260 d2t: Option<&[u32]>,
3261 sp: Option<&SpecSampling>,
3262 knobs: &mut Glm5SpecKnobs<'_>,
3263 mut phase: Option<&mut SpecPhaseNs>,
3264 ) -> Res<(Vec<u32>, usize)> {
3265 let n_vocab = self.output.out_features();
3266 let n_embd = self.cfg.n_embd as usize;
3267 // The MTP block / DFlash2 drafter, the trunk lm head and the verify walk's returned
3268 // rows all live on the LAST stage under a split — every draft-chain and accept-side
3269 // op below runs through the head engine (identity when the door is shut).
3270 let eh = self.glm5_head_engine(e)?;
3271 let mut t_mark = phase.as_ref().map(|_| SpecPhaseNs::clock(e, eh));
3272 // FIRST-TOKEN PROFILE (`MEMRA_SPEC_PROF=1`): round 1 only — the same drains as the
3273 // trace, bucketed into the session's one-shot profile instead of the per-burst
3274 // accumulator. `pclk` rides into the DFlash2 draft fn so the prompt ingest (the
3275 // drafter prime) gets its own bucket before the draft bucket starts.
3276 let mut pclk = (sess.rounds == 0 && sess.prof.is_some()).then(|| ProfClock::start(e, eh));
3277 // Phase-boundary bump: drain, bucket the elapsed ns, restart the clock. No-op with
3278 // the trace off (t_mark is None and no stream is ever synchronized).
3279 macro_rules! bump {
3280 ($field:ident) => {
3281 if let (Some(ph), Some(t0)) = (phase.as_deref_mut(), t_mark.as_mut()) {
3282 let now = SpecPhaseNs::clock(e, eh);
3283 ph.$field += now.duration_since(*t0).as_nanos() as u64;
3284 *t0 = now;
3285 }
3286 };
3287 }
3288 // Profile lap into the named first-round bucket (no-op with the profile off).
3289 macro_rules! plap {
3290 ($field:ident) => {
3291 if let (Some(ck), Some(pf)) = (pclk.as_mut(), sess.prof.as_mut()) {
3292 pf.$field = ck.lap(e, eh);
3293 }
3294 };
3295 }
3296
3297 // CONFIDENCE GATE resolution (loop-port 2): the env pair is the serving surface
3298 // (the step37 family, no new flags); the knobs override is the gate instrument.
3299 let (p_min, pmin0) = knobs
3300 .pmin_override
3301 .unwrap_or_else(|| (glm5_pmin(), glm5_pmin0()));
3302
3303 // ---- 1+2. produce the K drafts (+ the retained q side), SOURCE-KEYED. Everything
3304 // after this point is shared and source-blind — the exactness seam (module doc).
3305 let (drafts, qside, mtp_committed_len) = match sess.draft {
3306 Glm5DraftState::Dflash2 { .. } => {
3307 let (d, q) = self.glm5_dflash_round_drafts(
3308 eh,
3309 sess,
3310 k,
3311 sp,
3312 knobs,
3313 p_min,
3314 pmin0,
3315 pclk.as_mut(),
3316 )?;
3317 (d, q, 0)
3318 }
3319 Glm5DraftState::NativeMtp => {
3320 let mtp_il = sess.mtp_il.ok_or("native-mtp arm without a plane index")?;
3321 // ---- feed pending committed pairs; the last call yields draft 1 ----
3322 let mut last: Option<(CudaSlice<f32>, CudaSlice<f32>)> = None;
3323 for (tok, h) in sess.pending.drain(..) {
3324 let plane_len = sess.cache.latent[mtp_il]
3325 .as_ref()
3326 .ok_or("MTP plane missing")?
3327 .len;
3328 last = Some(self.mtp_head_forward_mla_cached(
3329 eh,
3330 0,
3331 tok,
3332 &h,
3333 &mut sess.cache,
3334 plane_len,
3335 )?);
3336 }
3337 let (mut d_logits, mut carrier) =
3338 last.ok_or("glm5 spec round started with no pending committed pair")?;
3339 let mtp_committed_len = sess.cache.latent[mtp_il]
3340 .as_ref()
3341 .ok_or("MTP plane missing")?
3342 .len;
3343
3344 // ---- chain K drafts. Greedy route: argmax over the draft head. Sampled
3345 // route: filtered Gumbel draw through the session's device Philox stream
3346 // (`sctr`), with the per-step filtered stats + logits retained — they are
3347 // the q side of the accept walk. Trimmed heads yield RANK ids that remap
3348 // through d2t to true vocab before anything consumes them (chain feed,
3349 // verify, output); the q gather keeps the rank id.
3350 let d_vocab = d2t.map(|m| m.len()).unwrap_or(n_vocab);
3351 let mut drafts: Vec<u32> = Vec::with_capacity(k); // true-vocab tokens
3352 let mut draft_idx: Vec<u32> = Vec::with_capacity(k); // draft-head rank ids
3353 let mut draft_logits: Vec<CudaSlice<f32>> = Vec::new(); // sampled route only
3354 let mut draft_stats: Vec<(f32, f32, f32)> = Vec::new(); // (mx, th, z), sampled only
3355 for ki in 0..k {
3356 let (idx, sampled_stats) = match sp {
3357 Some(sp) => {
3358 let (idx, stats) =
3359 glm5_sampled_draft(eh, &d_logits, d_vocab, sp, &mut sess.sctr)?;
3360 (idx, Some(stats))
3361 }
3362 None => {
3363 // Device argmax + ONE 4-byte readback per draft (loop-port 1)
3364 // — replaces the full d_vocab logits DtoH + host argmax the
3365 // map names at this seam. Same tie-break contract
3366 // (argmax_gate); drafts never decide exactness anyway, the
3367 // verify arbitrates. The #87 sentinel guard mirrors the
3368 // spec.rs graph chain: a device argmax may emit a sentinel
3369 // on a NaN row — refuse loudly, never gather an OOB embed.
3370 let td = eh.argmax_token_device(&d_logits, d_vocab)?;
3371 let idx = crate::spec::guard_vocab_token(
3372 eh.dtoh_u32_one(&td)?,
3373 d_vocab,
3374 &format!(
3375 "glm5 native draft argmax at round {} ki={ki}",
3376 sess.rounds
3377 ),
3378 )?;
3379 (idx, None)
3380 }
3381 };
3382 // P-MIN CONFIDENCE GATE (loop-port 2, the spec.rs chain break): p =
3383 // the head's softmax confidence in its own pick (the `g_p` statistic,
3384 // prob_of_token_device kernels), one 4-byte read — armed rounds only.
3385 // Break BEFORE the pick is drafted or the next full-MoE-layer chain
3386 // forward is paid; a discarded sampled draw's Philox advance stands
3387 // (spec.rs eager parity: "counts the p-min-discarded token too").
3388 if p_min > 0.0 {
3389 let tok_d = eh.htod_u32_v(&[idx])?;
3390 let p_d = eh.prob_of_token_device(&d_logits, &tok_d, d_vocab)?;
3391 let p = eh.dtoh(&p_d)?[0];
3392 if p < p_min && (ki > 0 || pmin0) {
3393 break;
3394 }
3395 }
3396 if let Some(stats) = sampled_stats {
3397 draft_stats.push(stats);
3398 draft_logits.push(eh.clone_dtod(&d_logits)?);
3399 }
3400 let mut d = match d2t {
3401 Some(map) if !knobs.skip_d2t_remap => map[idx as usize],
3402 _ => idx,
3403 };
3404 if let Some(over) = knobs.draft_override.as_mut() {
3405 d = over(sess.rounds, ki, d);
3406 }
3407 drafts.push(d);
3408 draft_idx.push(idx);
3409 if ki + 1 < k {
3410 let plane_len = sess.cache.latent[mtp_il]
3411 .as_ref()
3412 .ok_or("MTP plane missing")?
3413 .len;
3414 let (lg, ca) = self.mtp_head_forward_mla_cached(
3415 eh,
3416 0,
3417 d,
3418 &carrier,
3419 &mut sess.cache,
3420 plane_len,
3421 )?;
3422 d_logits = lg;
3423 carrier = ca;
3424 }
3425 }
3426 let q = match sp {
3427 Some(_) => Glm5DraftQ::Mtp {
3428 draft_idx,
3429 draft_logits,
3430 draft_stats,
3431 },
3432 None => Glm5DraftQ::None,
3433 };
3434 (drafts, q, mtp_committed_len)
3435 }
3436 };
3437 bump!(draft);
3438 plap!(first_draft_ms);
3439 // RANK-TRIM COUNTER (lane/frspec-dflash2-20260902): this round drafted through a
3440 // rank-trimmed head (`d2t` is the source-keyed map the drafts were produced under ,
3441 // the DFlash2 slab or the native trimmed chain). Counted on the head, not the remap:
3442 // the skip_d2t_remap red arm still ran over the slab.
3443 if d2t.is_some() {
3444 sess.rank_trimmed_rounds += 1;
3445 GLM5_RANK_TRIMMED_DRAFT_ROUNDS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3446 }
3447
3448 // DFlash2 source: arm the verify tap — the walk's rows are next round's drafter
3449 // context features (rows 0..keep survive the accept; the sink is taken in step 7).
3450 // DEVICE-STAGED (loop-port 1): the walk D2Ds each tapped layer's contracted rows
3451 // instead of blocking on five in-walk DtoHs; step 7 drains post-walk.
3452 if let Glm5DraftState::Dflash2 { taps, .. } = &sess.draft {
3453 sess.cache.hc_taps = Some(HcTapSink::new_device_staged(
3454 taps.clone(),
3455 n_embd,
3456 drafts.len() + 1,
3457 ));
3458 }
3459
3460 // ---- 3. verify: one t=K+1 walk over the trunk ----
3461 let mut rows: Vec<u32> = Vec::with_capacity(drafts.len() + 1);
3462 rows.push(sess.anchor);
3463 rows.extend_from_slice(&drafts);
3464 let (vlogits, collapsed, ckpt) = self.glm5_verify_rows(e, &rows, &mut sess.cache)?;
3465 bump!(verify);
3466 plap!(first_verify_ms);
3467
3468 // ---- 3b. PENALTY ARM (lane/spec-exclusions-20260902, MEMRA_SPEC_PENALTY=1) ----
3469 // The target rows are penalized IN A COPY over the per-row EVOLVING window: row r
3470 // (the target at drafts[r]'s slot; row 0 = the anchor's) sees `pen_win ++
3471 // drafts[..r]` — the tokens committed before that position on every path where the
3472 // row is consulted, same-round accepts included — through `penalize_logits_rows_inc`
3473 // (the dspark accept walk's kernel, host-order arithmetic). Every p read below —
3474 // the greedy argmaxes, the sampled stats/gather, the bonus row, the reject-slot
3475 // residual — points at that copy, so the accepted stream is the plain penalized
3476 // sampler's target: greedy byte-identical (gate 15), sampled distribution-exact.
3477 // The anchor joins the window first (it was emitted last round, committed by this
3478 // one: the dspark `pen_hist.push(last)` convention); accepted drafts join after the
3479 // accept. `pen: None` = no copy, no launch — `plogits` IS `vlogits`, the pre-lane
3480 // program byte for byte.
3481 let pvl: Option<CudaSlice<f32>> = match sess.pen {
3482 Some(p) => {
3483 sess.pen_hist.push(sess.anchor);
3484 let w0 = sess.pen_hist.len().saturating_sub(p.win());
3485 let pen_win = &sess.pen_hist[w0..];
3486 let mut hist: Vec<u32> = Vec::with_capacity(pen_win.len() + drafts.len());
3487 hist.extend_from_slice(pen_win);
3488 hist.extend_from_slice(&drafts);
3489 let n_win = pen_win.len();
3490 let hd = eh.htod_u32_v(&hist)?;
3491 let mut buf = eh.clone_dtod(&vlogits)?;
3492 eh.penalize_logits_rows_inc(
3493 &mut buf,
3494 &hd,
3495 n_win,
3496 p.rep,
3497 p.freq,
3498 p.present,
3499 n_vocab,
3500 rows.len(),
3501 p.win(),
3502 )?;
3503 Some(buf)
3504 }
3505 None => None,
3506 };
3507 let plogits: &CudaSlice<f32> = pvl.as_ref().unwrap_or(&vlogits);
3508 // The dspark accept walk penalizes internally off `sp.pen_on()`; under the arm it
3509 // receives the ALREADY penalized rows and a penalty-neutral config, so the round has
3510 // exactly one penalty pass. With the arm off this is `sp` itself, untouched.
3511 let sp_accept: Option<SpecSampling> = sp.map(|s| match sess.pen {
3512 Some(_) => SpecSampling {
3513 penalty_last_n: 0,
3514 ..*s
3515 },
3516 None => *s,
3517 });
3518
3519 // ---- 4. accept ----
3520 // ZERO-DRAFT SAMPLED ROUND (PMIN0): the verify batch is just the anchor row —
3521 // m=1 = a plain decode step, exactly the llama.cpp gating spec.rs vendored. The
3522 // bonus is the full-accept filtered-Gumbel draw from that one row through the
3523 // session's Philox stream (identical in distribution to the plain sampled step
3524 // this round degenerates to). Greedy zero-draft rounds ride the general arm
3525 // below (j=0, bonus = the row-0 device argmax).
3526 let (j, bonus) = if let (true, Some(sp)) = (drafts.is_empty(), sp) {
3527 (
3528 0,
3529 self.glm5_sampled_bonus(eh, sess, sp, plogits, 0, n_vocab)?,
3530 )
3531 } else {
3532 match (sp, &qside) {
3533 (None, _) => {
3534 // Greedy longest matching prefix (the DFlash2 probe's rule); bonus = the
3535 // target's own argmax at the first non-accepted slot. Byte-deterministic —
3536 // the instrument the spec-vs-plain identity gates pin.
3537 //
3538 // DEVICE ACCEPT ARGMAXES (loop-port 1, the K=1 flip): per verify row, a
3539 // device argmax into one [t] slot buffer, then ONE tiny u32 readback —
3540 // replacing the (K+1) x n_vocab logits DtoH + (K+1) host argmax scans
3541 // (~2.4 MB + a host walk over 600k floats at K=3 on the real head; the
3542 // 3way arithmetic needs 0.67 ms off the fixed round cost to flip K=1).
3543 // `argmax_token_device_col` carries the host argmax's tie-break contract
3544 // bit for bit (lowest index wins, argmax_gate-validated), so the accept
3545 // walk commits the SAME tokens in the SAME order — the byte-identity
3546 // batteries below stay the proof.
3547 let t = rows.len();
3548 let mut vam_d = eh.alloc_u32_zeroed(t)?;
3549 for r in 0..t {
3550 eh.argmax_token_device_col(plogits, r, n_vocab, &mut vam_d, r)?;
3551 }
3552 let vam = eh.dtoh_u32(&vam_d)?;
3553 let mut j = 0usize;
3554 while j < drafts.len() && drafts[j] == vam[j] {
3555 j += 1;
3556 }
3557 if knobs.accept_probe {
3558 self.glm5_accept_probe(eh, sess.rounds, plogits, &drafts, &vam, j)?;
3559 }
3560 (j, vam[j])
3561 }
3562 (
3563 Some(sp),
3564 Glm5DraftQ::Mtp {
3565 draft_idx,
3566 draft_logits,
3567 draft_stats,
3568 },
3569 ) => self.glm5_sampled_accept(
3570 eh,
3571 sess,
3572 sp,
3573 plogits,
3574 &drafts,
3575 draft_idx,
3576 draft_logits,
3577 draft_stats,
3578 d2t,
3579 drafts.len(),
3580 )?,
3581 (Some(_), Glm5DraftQ::Selector { prop, dl }) => {
3582 // The q38 serve route's rejection walk, VERBATIM (`dspark_accept_sampled`):
3583 // `rows` = [anchor, drafts..] is its cand contract, verify row j arbitrates
3584 // rows[j+1], the bonus draws from row k on full accept, and the reject-slot
3585 // residual uses the selector's sparse candidate-set q. Philox counters are
3586 // this session's — randomness never repeats across bursts.
3587 let (m, next) = crate::dflash::dspark_accept_sampled(
3588 eh,
3589 plogits,
3590 &rows,
3591 rows.len(),
3592 n_vocab,
3593 dl,
3594 prop,
3595 sp_accept.as_ref().expect("sampled arm carries its config"),
3596 &[],
3597 &mut sess.sctr,
3598 &mut sess.uctr,
3599 )?;
3600 let next = crate::spec::guard_vocab_token(
3601 next,
3602 n_vocab,
3603 &format!(
3604 "glm5 dflash2 sampled verify bonus at round {} j={m}",
3605 sess.rounds
3606 ),
3607 )?;
3608 (m, next)
3609 }
3610 (Some(_), Glm5DraftQ::None) => {
3611 unreachable!("sampled round without a retained q side")
3612 }
3613 }
3614 };
3615 // Accepted drafts join the penalty window (the bonus joins as next round's anchor).
3616 if sess.pen.is_some() {
3617 sess.pen_hist.extend_from_slice(&drafts[..j]);
3618 }
3619 bump!(accept);
3620 plap!(first_accept_ms);
3621
3622 // ---- 5. commit j drafts + the bonus token ----
3623 let mut round_tokens: Vec<u32> = Vec::with_capacity(j + 1);
3624 round_tokens.extend_from_slice(&drafts[..j]);
3625 round_tokens.push(bonus);
3626
3627 // ---- 6. rollback the trunk to the accepted prefix ----
3628 let keep = j + 1;
3629 if knobs.disable_rollback {
3630 // RED-ARM INSTRUMENT: move pos, leave every state plane at post-row-K.
3631 sess.cache.pos = ckpt.pos + keep;
3632 } else {
3633 self.glm5_verify_rollback(e, &mut sess.cache, &ckpt, keep)?;
3634 }
3635 bump!(roll);
3636 plap!(first_roll_ms);
3637
3638 // ---- 7+8. draft-source state maintenance, SOURCE-KEYED ----
3639 match &mut sess.draft {
3640 Glm5DraftState::NativeMtp => {
3641 // MTP plane: len reset to the committed boundary (chain rows out), then
3642 // re-seed the pending pairs (token at pos0+i, collapsed row i-1).
3643 self.glm5_mtp_plane_reset(e, &mut sess.cache, mtp_committed_len)?;
3644 for i in 1..=keep {
3645 let tok = round_tokens[i - 1];
3646 let h = self.glm5_seed_row(eh, &collapsed, rows.len(), i - 1)?;
3647 sess.pending.push((tok, h));
3648 }
3649 }
3650 Glm5DraftState::Dflash2 { pending, taps, .. } => {
3651 // The kept verify rows' tap features (rows 0..keep = [anchor, accepted
3652 // drafts]) become next round's drafter context — the probe's
3653 // `F_feat[new_lo:start]` advance. The drafter's own KV block rows were
3654 // transient (forward_round never moves kv.len), so no drafter rollback
3655 // exists to run. The trunk-side MTP plane was never touched.
3656 // Device-staged rows drain HERE — the round's one post-walk sync point
3657 // for tap features (loop-port 1).
3658 let mut sink = sess
3659 .cache
3660 .hc_taps
3661 .take()
3662 .ok_or("glm5 dflash verify tap sink vanished")?;
3663 self.glm5_tap_drain(e, &mut sink)?;
3664 let row_w = taps.len() * n_embd;
3665 pending.extend_from_slice(&sink.rows[..keep * row_w]);
3666 }
3667 }
3668 // Cache-row bookkeeping: the trunk committed rows [anchor, drafts[..j]] (keep =
3669 // j+1), so `committed` gains exactly those tokens; the BONUS is the new live
3670 // anchor — emitted this round, consumed by the trunk as the NEXT round's row 0
3671 // (the dspark `last` convention). Invariant at every round boundary:
3672 // `cache.pos == committed.len()`, token-for-token.
3673 sess.committed.push(sess.anchor);
3674 sess.committed.extend_from_slice(&drafts[..j]);
3675 sess.anchor = bonus;
3676 bump!(maint);
3677 plap!(first_maint_ms);
3678 if let Some(pf) = sess.prof.as_mut()
3679 && pclk.is_some()
3680 {
3681 pf.first_round_tokens = round_tokens.len();
3682 }
3683 if let Some(ph) = phase {
3684 ph.rounds += 1;
3685 }
3686 Ok((round_tokens, drafts.len()))
3687 }
3688
3689 /// THE ACCEPTANCE-RACE FIX (lane/glm5-accrace 2026-09-01): order the CALLER's stream
3690 /// behind EVERY stage stream — the exit mirror of
3691 /// [`crate::pp::PpNRt::fence_stages_behind`], built from
3692 /// [`crate::pp::PpNRt::publish_all_to`] (event waits, never a device sync, so the stage
3693 /// streams keep running).
3694 ///
3695 /// Call OUTSIDE any `rt.enter` scope: `e.stream()` must resolve to the caller's stream,
3696 /// not a stage's. Door shut or the same-stream seam (`MEMRA_PP_STREAMS=0`): a no-op by
3697 /// construction, so single-device and STREAMS=0 behaviour is untouched.
3698 fn glm5_publish_stages(&self, e: &Engine) -> Res<()> {
3699 if crate::pp::pp_cuts(self.layers.len()).is_some() && !crate::pp::pp2_streams_off() {
3700 let rt = crate::pp::PpNRt::get(e)?;
3701 let dst = e.stream();
3702 rt.publish_all_to(&dst)?;
3703 }
3704 Ok(())
3705 }
3706
3707 /// GATE INSTRUMENT (lane/glm5-accrace; contract in [`Glm5SpecKnobs::accept_probe`]):
3708 /// one stderr line per greedy round pairing the DEVICE accept row against a HOST
3709 /// argmax over the same buffer, plus a per-row (argmax, row hash) census so two runs
3710 /// of the same deterministic fixture can be diffed round-for-round.
3711 fn glm5_accept_probe(
3712 &self,
3713 eh: &Engine,
3714 round: usize,
3715 vlogits: &CudaSlice<f32>,
3716 drafts: &[u32],
3717 vam: &[u32],
3718 j: usize,
3719 ) -> Res<()> {
3720 let n_vocab = self.output.out_features();
3721 let t = vam.len();
3722 let host = eh.dtoh(vlogits)?;
3723 let mut hvam: Vec<u32> = Vec::with_capacity(t);
3724 let mut rows_census: Vec<String> = Vec::with_capacity(t);
3725 for r in 0..t {
3726 let row = &host[r * n_vocab..(r + 1) * n_vocab];
3727 let am = argmax(row) as u32;
3728 hvam.push(am);
3729 // FNV-1a over the row's f32 BITS: a bit-level fingerprint, so a run-to-run
3730 // diff is exact rather than eyeballed at some print precision.
3731 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
3732 for v in row {
3733 for b in v.to_bits().to_le_bytes() {
3734 h ^= u64::from(b);
3735 h = h.wrapping_mul(0x100_0000_01b3);
3736 }
3737 }
3738 rows_census.push(format!("{r}:{am}:{h:016x}"));
3739 }
3740 eprintln!(
3741 "[glm5-accrace] round={round} t={t} j={j} keep={} drafts={drafts:?} \
3742 dev_vam={vam:?} host_vam={hvam:?} agree={} rows=[{}]",
3743 j + 1,
3744 hvam == vam,
3745 rows_census.join(" ")
3746 );
3747 Ok(())
3748 }
3749
3750 /// ONE round's drafts from the DFlash2 source (module doc, DRAFT SOURCE SEAM) — the
3751 /// shipped q38 selector round, re-aimed at glm5's hc-contract features:
3752 ///
3753 /// 1. ingest pending committed feature rows into the drafter's own ctx KV (chunked
3754 /// at 256 rows — the qwen depth-OOM bound; round 1 carries the whole prompt);
3755 /// 2. block forward `[anchor, MASK x b-1]` at absolute positions over the cached ctx
3756 /// (`forward_round` — block K/V transient, exactly the reference crop);
3757 /// 3. draft logits = trunk lm_head over rows 1..b (mask-fill harvest — the DFlash2
3758 /// family census; FR-Spec trim consumed exactly as the dspark serve arm does);
3759 /// 4. selector walk: greedy chain, or the sampled candidate-set walk whose recorded
3760 /// q (`DsparkDraftSample::Selector`) the shared rejection accept consumes.
3761 ///
3762 /// Drafts are truncated to `k` (the chain is sequential, so a prefix is well-formed),
3763 /// then to the CONFIDENCE prefix when `p_min` is armed (loop-port 2, the tau-slot
3764 /// form): the selector's recorded per-slot q — `q_chosen` on the sampled walk, its
3765 /// T=1 twin on the greedy walk — gates each slot through `glm5_conf_keep`, so the
3766 /// low-confidence tail never enters the verify batch (a truncated round rides down
3767 /// the `31.6 + 20.1*K` line; rejection sampling stays exact for any proposal prefix).
3768 /// `knobs.draft_override` applies after — the gate instrument, never serving.
3769 #[allow(clippy::too_many_arguments)]
3770 // allow: the parameter list mirrors the round contract plus the resolved gate pair
3771 fn glm5_dflash_round_drafts(
3772 &self,
3773 eh: &Engine,
3774 sess: &mut Glm5SpecSession,
3775 k: usize,
3776 sp: Option<&SpecSampling>,
3777 knobs: &mut Glm5SpecKnobs<'_>,
3778 p_min: f32,
3779 pmin0: bool,
3780 pclk: Option<&mut ProfClock>,
3781 ) -> Res<(Vec<u32>, Glm5DraftQ)> {
3782 let dr = self
3783 .glm5_dflash
3784 .as_ref()
3785 .ok_or("glm5 dflash draft state without a loaded drafter")?;
3786 let draft = &dr.draft;
3787 let c = &draft.cfg;
3788 let b = c.block_size;
3789 let n_embd = self.cfg.n_embd as usize;
3790 let n_vocab = self.output.out_features();
3791 let Glm5SpecSession {
3792 draft: state,
3793 cache,
3794 anchor,
3795 sctr: _,
3796 uctr,
3797 rounds,
3798 prof,
3799 ..
3800 } = sess;
3801 let Glm5DraftState::Dflash2 { kv, pending, taps } = state else {
3802 return Err("glm5_dflash_round_drafts on a native-mtp session".into());
3803 };
3804 let anchor = *anchor;
3805
3806 // ---- 1. ingest pending committed feature rows (positions kv.len..) ----
3807 // Under the default placement the PROMPT rows were ingested at session creation
3808 // and only the kept verify rows of the previous round arrive here; under
3809 // `MEMRA_GLM5_DRAFT_PRIME_LAZY=1` round 1 carries the whole prompt (the pre-lane
3810 // literal, the round-0 wall boot A measured at depth).
3811 let row_w = taps.len() * n_embd;
3812 let mut pclk = pclk;
3813 let n_new = pending.len() / row_w;
3814 let st = if n_new > 0 {
3815 let rows = std::mem::take(pending);
3816 self.glm5_dflash_ingest_rows(eh, draft, kv, &rows, row_w, pclk.as_deref_mut())?
3817 } else {
3818 DraftIngestStats::default()
3819 };
3820 // FIRST-TOKEN PROFILE: the lazy arm's round-1 prompt ingest lands in the
3821 // drafter-prime buckets here (a creation-time ingest already wrote them and
3822 // leaves nothing prompt-sized pending); the clock is re-based either way.
3823 if let (Some(ck), Some(pf)) = (pclk, prof.as_mut()) {
3824 let tail = ck.lap(eh, eh);
3825 if *rounds == 0 && n_new > 0 && pf.draft_prime_arm.is_empty() {
3826 st.write(pf, "eager-lazy");
3827 pf.draft_prime_ms += tail;
3828 }
3829 }
3830 let start = cache.pos;
3831 debug_assert_eq!(
3832 kv.len, start,
3833 "drafter ctx rows must equal committed trunk rows at a round boundary"
3834 );
3835
3836 // ---- 2. block forward over the cached ctx (decode-exact matmul scope: the m=8
3837 // drafter GEMMs otherwise fall into the prefill-GEMM class — the dspark round's
3838 // measured fix; RAII so a `?` exit never latches exact engine-wide) ----
3839 let exact_scope = eh.exact_scope(true);
3840 let mut block: Vec<u32> = vec![c.mask_token_id; b];
3841 block[0] = anchor;
3842 let noise = eh.htod(&self.embd.try_gather(n_embd, &block)?)?;
3843 let pos_block: Vec<i32> = ((start as i32)..(start + b) as i32).collect();
3844 let dh = draft.forward_round(eh, kv, &noise, &pos_block)?;
3845
3846 // ---- 3. draft logits over the mask-fill harvest rows 1..b ----
3847 let nd = b - 1;
3848 let mut rows_buf = eh.uninit(nd * n_embd)?;
3849 {
3850 let dv = eh.view(&dh, b * n_embd);
3851 let tail = dv.slice(n_embd..b * n_embd);
3852 eh.copy_view_into(&mut rows_buf, 0, &tail, nd * n_embd)?;
3853 }
3854 // TRIMMED DRAFT HEAD (`glm5_dflash_trim`): the dspark serve arm's resolution, the
3855 // FR-Spec self-trim on the MTP struct when it loaded (gathered rows of the target's
3856 // own head), else the `dflash_trim` slab the loader builds for a DFlash2 route that
3857 // boots WITHOUT the NextN block (lane/frspec-dflash2-20260902, the serving shape;
3858 // before it the `MEMRA_FRSPEC_TRIM` contract was a silent no-op here). Neither = the
3859 // full target head, stated in the boot receipt. The draft logits `dl` arrive
3860 // `[nd x n_ranks]`; the propose fns remap candidate RANK ids through d2t before the
3861 // selector walk; verify stays full-vocab, so the slab moves acceptance only.
3862 let trim = self.glm5_dflash_trim();
3863 let (dl_head, dl_vocab) = match trim {
3864 Some((head, d2t)) => (head, d2t.len()),
3865 None => (&self.output, n_vocab),
3866 };
3867 // skip_d2t_remap red arm (the q38 defect made loud): candidates stay RANK ids.
3868 let trim_d2t = trim.filter(|_| !knobs.skip_d2t_remap).map(|(_, d2t)| d2t);
3869 let dl = eh.matmul(dl_head, &rows_buf, nd)?;
3870
3871 // ---- 4. selector walk (greedy chain / sampled candidate-set walk) ----
3872 let (mut drafts, slot_q, qside) = match sp {
3873 None => {
3874 let (path, q) = draft
3875 .dflash2_propose_greedy_q(eh, &dl, &rows_buf, nd, dl_vocab, anchor, trim_d2t)?;
3876 (path, q, Glm5DraftQ::None)
3877 }
3878 Some(sp) => {
3879 let (path, q_chosen, cand, q_rows) = draft.dflash2_propose_sampled(
3880 eh, &dl, &rows_buf, nd, dl_vocab, anchor, sp.temp, sp.seed, uctr, trim_d2t,
3881 )?;
3882 let top_k = draft
3883 .dflash2
3884 .as_ref()
3885 .ok_or("glm5 dflash drafter lost its DFlash2 head")?
3886 .top_k;
3887 (
3888 path,
3889 q_chosen.clone(),
3890 Glm5DraftQ::Selector {
3891 prop: DsparkDraftSample::Selector {
3892 cand,
3893 q_rows,
3894 q_chosen,
3895 top_k,
3896 },
3897 dl,
3898 },
3899 )
3900 }
3901 };
3902 drop(exact_scope);
3903 drafts.truncate(k);
3904 // TAU-SLOT CONFIDENCE TRUNCATION (loop-port 2): the low-confidence tail never
3905 // enters verify. Slot-indexed prefix reads keep the retained Selector q side
3906 // consistent (cand/q_rows/q_chosen are per-slot; the accept walk reads slots
3907 // 0..drafts.len()-1 only). p_min unset = today's rounds, untouched.
3908 if p_min > 0.0 {
3909 let kc = glm5_conf_keep(&slot_q[..drafts.len()], p_min, pmin0);
3910 drafts.truncate(kc);
3911 }
3912 if let Some(over) = knobs.draft_override.as_mut() {
3913 for (ki, d) in drafts.iter_mut().enumerate() {
3914 *d = over(*rounds, ki, *d);
3915 }
3916 }
3917 Ok((drafts, qside))
3918 }
3919
3920 /// SAMPLED ACCEPT (module doc): the rejection-sampling walk `u_j * q_j(x_j) < p_j(x_j)`
3921 /// over the verify logit rows — memra's existing sampled spec contract (the
3922 /// MEMRA_SPEC_TEMP route / dspark sampled-admission walk), plugged in at exactly the
3923 /// accept seam; walk and rollback unchanged. p and q take the SAME filter transforms
3924 /// (`filter_stats` + `softmax_gather_filtered`, distribution-exact for the filtered
3925 /// target); the accept-test uniforms come from `spec::host_u01` on the session's `uctr`
3926 /// (tag 0xFFFF_FFFE) and every device draw (draft chain, full-accept bonus, residual
3927 /// resample) advances the session's `sctr` — counters persist on the session so
3928 /// randomness never repeats across bursts. Returns `(j, bonus)`. `e` is the HEAD
3929 /// engine (the round resolves it): the verify rows and retained draft logits live on
3930 /// the last stage under a split.
3931 #[allow(clippy::too_many_arguments)]
3932 // allow: the parameter list mirrors the accept seam's inputs (verify rows + the draft
3933 // chain's retained q side); bundling into a struct would hide the p/q pairing
3934 fn glm5_sampled_accept(
3935 &self,
3936 e: &Engine,
3937 sess: &mut Glm5SpecSession,
3938 sp: &SpecSampling,
3939 vlogits: &CudaSlice<f32>,
3940 drafts: &[u32],
3941 draft_idx: &[u32],
3942 draft_logits: &[CudaSlice<f32>],
3943 draft_stats: &[(f32, f32, f32)],
3944 d2t: Option<&[u32]>,
3945 k: usize,
3946 ) -> Res<(usize, u32)> {
3947 let n_vocab = self.output.out_features();
3948 let d_vocab = d2t.map(|m| m.len()).unwrap_or(n_vocab);
3949 // FILTERED p_j: one batched stats pass over verify rows 0..k-1 (row j is the target
3950 // distribution at draft j's slot), then one batched gather of the drafted tokens.
3951 let rows_i: Vec<i32> = (0..k as i32).collect();
3952 let rows_d = e.htod_i32(&rows_i)?;
3953 let (mut th_d, mut z_d, mut mx_d) = (e.zeros(k)?, e.zeros(k)?, e.zeros(k)?);
3954 e.filter_stats(
3955 vlogits, n_vocab, &rows_d, &mut th_d, &mut z_d, &mut mx_d, n_vocab, k, sp.temp,
3956 sp.top_k, sp.top_p, sp.min_p,
3957 )?;
3958 let ids_d = e.htod_u32_v(drafts)?;
3959 let mut pj_d = e.zeros(k)?;
3960 e.softmax_gather_filtered(
3961 vlogits, n_vocab, &ids_d, &rows_d, &th_d, &z_d, &mut pj_d, n_vocab, k, sp.temp,
3962 )?;
3963 let pj = e.dtoh(&pj_d)?;
3964 let (thv, zv, mxv) = (e.dtoh(&th_d)?, e.dtoh(&z_d)?, e.dtoh(&mx_d)?);
3965
3966 // The walk: FILTERED q_j from the retained draft logits (rank id for trimmed heads),
3967 // host Philox accept test per slot.
3968 let mut j = 0usize;
3969 while j < k {
3970 let (_qmx, qth, qz) = draft_stats[j];
3971 let idsd = e.htod_u32_v(&[draft_idx[j]])?;
3972 let rows0 = e.htod_i32(&[0])?;
3973 let thd = e.htod(&[qth])?;
3974 let zd = e.htod(&[qz])?;
3975 let mut outd = e.zeros(1)?;
3976 e.softmax_gather_filtered(
3977 &draft_logits[j],
3978 d_vocab,
3979 &idsd,
3980 &rows0,
3981 &thd,
3982 &zd,
3983 &mut outd,
3984 d_vocab,
3985 1,
3986 sp.temp,
3987 )?;
3988 let qj = e.dtoh(&outd)?[0];
3989 let u = crate::spec::host_u01(sp.seed, sess.uctr);
3990 sess.uctr = sess.uctr.wrapping_add(1);
3991 if (u as f64) * (qj as f64) < pj[j] as f64 {
3992 j += 1;
3993 } else {
3994 break;
3995 }
3996 }
3997
3998 // Bonus: full accept draws a filtered Gumbel sample from the LAST verify row
3999 // (`glm5_sampled_bonus` — shared with the PMIN0 zero-draft round); rejection at j
4000 // resamples the residual norm(max(0, fp_j - fq_j)) — with a trimmed draft head, q
4001 // scatters back to full vocab first (`scatter_trim_logits`).
4002 if j == k {
4003 return Ok((
4004 j,
4005 self.glm5_sampled_bonus(e, sess, sp, vlogits, k, n_vocab)?,
4006 ));
4007 }
4008 let mut col = e.zeros(n_vocab)?;
4009 let bonus = {
4010 let vv = e.view(vlogits, (k + 1) * n_vocab);
4011 let row = vv.slice(j * n_vocab..(j + 1) * n_vocab);
4012 e.copy_view_into(&mut col, 0, &row, n_vocab)?;
4013 let p_stats = (mxv[j], thv[j], zv[j]);
4014 let q_stats = draft_stats[j];
4015 let sc = sess.sctr;
4016 sess.sctr = sess.sctr.wrapping_add(1);
4017 let mut sample_tok = e.alloc_u32_zeroed(1)?;
4018 match d2t {
4019 Some(map) => {
4020 let map_d = e.htod_u32_v(map)?;
4021 let mut q_full = e.zeros(n_vocab)?;
4022 e.scatter_trim_logits(&draft_logits[j], &map_d, &mut q_full, d_vocab, n_vocab)?;
4023 e.residual_sample_filtered(
4024 &col,
4025 Some(&q_full),
4026 n_vocab,
4027 sp.temp,
4028 sp.seed,
4029 sc,
4030 p_stats,
4031 q_stats,
4032 &mut sample_tok,
4033 )?;
4034 }
4035 None => {
4036 e.residual_sample_filtered(
4037 &col,
4038 Some(&draft_logits[j]),
4039 n_vocab,
4040 sp.temp,
4041 sp.seed,
4042 sc,
4043 p_stats,
4044 q_stats,
4045 &mut sample_tok,
4046 )?;
4047 }
4048 }
4049 e.dtoh_u32(&sample_tok)?[0]
4050 };
4051 let bonus = crate::spec::guard_vocab_token(
4052 bonus,
4053 n_vocab,
4054 &format!("glm5 sampled verify bonus at round {} j={j}", sess.rounds),
4055 )?;
4056 Ok((j, bonus))
4057 }
4058
4059 /// One filtered-Gumbel bonus draw from verify row `row` through the session's device
4060 /// Philox stream — the sampled FULL-ACCEPT bonus, and the entire accept of a PMIN0
4061 /// zero-draft round (whose verify batch is just the anchor row: m=1 = a plain sampled
4062 /// decode step). Advances `sctr` exactly once; byte-for-byte the pre-extraction
4063 /// full-accept arm of `glm5_sampled_accept`.
4064 fn glm5_sampled_bonus(
4065 &self,
4066 e: &Engine,
4067 sess: &mut Glm5SpecSession,
4068 sp: &SpecSampling,
4069 vlogits: &CudaSlice<f32>,
4070 row: usize,
4071 n_vocab: usize,
4072 ) -> Res<u32> {
4073 let mut col = e.zeros(n_vocab)?;
4074 let vv = e.view(vlogits, (row + 1) * n_vocab);
4075 let src = vv.slice(row * n_vocab..(row + 1) * n_vocab);
4076 e.copy_view_into(&mut col, 0, &src, n_vocab)?;
4077 let rows0 = e.htod_i32(&[0])?;
4078 let (mut bth, mut bz, mut bmx) = (e.zeros(1)?, e.zeros(1)?, e.zeros(1)?);
4079 e.filter_stats(
4080 &col, n_vocab, &rows0, &mut bth, &mut bz, &mut bmx, n_vocab, 1, sp.temp, sp.top_k,
4081 sp.top_p, sp.min_p,
4082 )?;
4083 let (th, mx) = (e.dtoh(&bth)?[0], e.dtoh(&bmx)?[0]);
4084 let mut pb = e.zeros(n_vocab)?;
4085 e.gumbel_perturb_filtered(&col, &mut pb, n_vocab, sp.seed, sess.sctr, sp.temp, mx, th)?;
4086 sess.sctr = sess.sctr.wrapping_add(1);
4087 let td = e.argmax_token_device(&pb, n_vocab)?;
4088 crate::spec::guard_vocab_token(
4089 e.dtoh_u32_one(&td)?,
4090 n_vocab,
4091 &format!(
4092 "glm5 sampled verify bonus at round {} (row {row})",
4093 sess.rounds
4094 ),
4095 )
4096 }
4097}
4098
4099/// One filtered Gumbel draw from a draft-head logit row through the session's device Philox
4100/// stream — the sampled route's PROPOSAL. Returns the drawn RANK id and the row's filtered
4101/// stats `(row_max, threshold_e, renorm_mass)`, which the accept walk's q gather and the
4102/// rejection residual both reuse (the q side must be the distribution the draft was actually
4103/// drawn from, or rejection sampling is not exact for the filtered target).
4104fn glm5_sampled_draft(
4105 e: &Engine,
4106 dl: &CudaSlice<f32>,
4107 d_vocab: usize,
4108 sp: &SpecSampling,
4109 sctr: &mut u32,
4110) -> Res<(u32, (f32, f32, f32))> {
4111 let rows0 = e.htod_i32(&[0])?;
4112 let (mut th_d, mut z_d, mut mx_d) = (e.zeros(1)?, e.zeros(1)?, e.zeros(1)?);
4113 e.filter_stats(
4114 dl, d_vocab, &rows0, &mut th_d, &mut z_d, &mut mx_d, d_vocab, 1, sp.temp, sp.top_k,
4115 sp.top_p, sp.min_p,
4116 )?;
4117 let (th, z, mx) = (e.dtoh(&th_d)?[0], e.dtoh(&z_d)?[0], e.dtoh(&mx_d)?[0]);
4118 let mut pb = e.zeros(d_vocab)?;
4119 e.gumbel_perturb_filtered(dl, &mut pb, d_vocab, sp.seed, *sctr, sp.temp, mx, th)?;
4120 *sctr = sctr.wrapping_add(1);
4121 let td = e.argmax_token_device(&pb, d_vocab)?;
4122 let idx =
4123 crate::spec::guard_vocab_token(e.dtoh_u32_one(&td)?, d_vocab, "glm5 sampled draft draw")?;
4124 Ok((idx, (mx, th, z)))
4125}
4126
4127/// glm5_next SERVED speculative session (lane/glm5-spec-routing, 2026-08-30): the state one
4128/// request's spec decoding carries across worker bursts — the dspark/gemma session twins'
4129/// shape. The session OWNS its trunk cache (the worker's `s.cache` stays `None`); at every
4130/// burst boundary the invariant is `cache.pos == committed.len()` with each committed row's
4131/// trunk state exactly what a plain prime of that sequence would hold (the accept walk's
4132/// basis, pinned by the tparallel gate), plus ONE emitted-but-uncommitted `anchor` token
4133/// (the next round's verify row 0 — the dspark `last` convention).
4134pub struct Glm5SpecSession {
4135 cache: Cache,
4136 /// Every token whose trunk state the cache holds, in order (prompt + committed
4137 /// generation). EXCLUDES the live `anchor`.
4138 pub committed: Vec<u32>,
4139 /// The last emitted token, not yet consumed by the trunk — round anchor / verify row 0.
4140 anchor: u32,
4141 /// The prime's boundary token is emitted exactly once, by the first burst.
4142 anchor_emitted: bool,
4143 /// Committed `(token, h_seed)` pairs not yet fed to the MTP draft plane; the last
4144 /// feed's logits double as the next round's first draft (the re-warm contract).
4145 /// NATIVE-MTP arm only; the DFlash2 source keeps its own pending rows in `draft`.
4146 pending: Vec<(u32, CudaSlice<f32>)>,
4147 /// The session's pinned draft source + its state (module doc, DRAFT SOURCE SEAM).
4148 draft: Glm5DraftState,
4149 /// `None` / `temp <= 0` = greedy byte-contract route. Fixed for the session — the
4150 /// worker's admission owns the sampler identity.
4151 sampling: Option<SpecSampling>,
4152 /// PENALTY ARM (lane/spec-exclusions-20260902, `MEMRA_SPEC_PENALTY=1`): the request's
4153 /// penalty config when it carries one — independent of `sampling` (a greedy request
4154 /// with penalties is `sampling: None, pen: Some`). `None` = no penalty pass anywhere on
4155 /// the round, the pre-lane program byte for byte.
4156 pen: Option<Glm5Penalty>,
4157 /// The session-spanning penalty history (`pen_window_seed` over the prompt, then the
4158 /// anchor at every round start and the accepted drafts after every accept — the dspark
4159 /// `pen_hist` convention); the verify rows penalize over its last `win()` entries.
4160 /// Empty with the arm off.
4161 pen_hist: Vec<u32>,
4162 /// Session-continuity Philox counters (never reset across bursts): `sctr` = device
4163 /// sampling events (boundary, draft chain, bonus, residual), `uctr` = host accept-test
4164 /// uniforms (`spec::host_u01`, tag 0xFFFF_FFFE).
4165 sctr: u32,
4166 uctr: u32,
4167 /// Verify rounds completed over the session lifetime (the worker's per-burst
4168 /// rounds-delta receipt, the dspark `rounds` convention).
4169 pub rounds: usize,
4170 /// Of `rounds`, those whose drafts came through a RANK-TRIMMED draft head (the DFlash2
4171 /// slab / MtpHead self-trim / native trimmed chain; lane/frspec-dflash2-20260902). Equals
4172 /// `rounds` when a trim is loaded, 0 otherwise, the counter the `[glm5-acc]` receipt
4173 /// and the rig gate read; a process-wide twin is `glm5_rank_trimmed_draft_rounds`.
4174 pub rank_trimmed_rounds: usize,
4175 done: bool,
4176 max_ctx: usize,
4177 /// MTP draft-plane layer index — `Some` on the native-MTP arm only.
4178 mtp_il: Option<usize>,
4179 /// Prompt-boundary capture for the DEFERRED prefix publication (lane/glm5-prefix-latent2,
4180 /// 2026-09-01; the dspark `prefix_capture` pattern): taken at session creation before any
4181 /// burst mutates the recurrent/tail state, drained by the worker's sweep. `None` when the
4182 /// worker did not request capture or when any boundary invariant refused.
4183 prefix_capture: Option<crate::spec::SpecBoundaryCapture>,
4184 /// FIRST-TOKEN PROFILE (lane/b200-spec-ttft-20260902): `Some` iff `MEMRA_SPEC_PROF=1`
4185 /// at creation; filled through session creation and round 1 of the first burst, then
4186 /// drained by the worker's one `[spec-prof]` line (`take_first_token_prof`).
4187 prof: Option<SpecFirstTokenProf>,
4188 /// DEPTH LOG (lane/spec-route-depth-20260902): the first `SPEC_PROF_ROUNDS` rounds'
4189 /// attribution rows, `Some` iff `MEMRA_SPEC_PROF=1`; the worker prints fresh rows after
4190 /// every burst and the summary once the log fills or the session ends.
4191 prof_rounds: Option<SpecRoundsLog>,
4192}
4193
4194impl Glm5SpecSession {
4195 /// The depth log, for the worker's `[spec-prof-rounds]` / `[spec-prof-summary]` lines.
4196 pub fn round_log_mut(&mut self) -> Option<&mut SpecRoundsLog> {
4197 self.prof_rounds.as_mut()
4198 }
4199 /// Rounds the depth log keeps (the print cadence's cap).
4200 pub fn round_log_cap() -> usize {
4201 SPEC_PROF_ROUNDS
4202 }
4203 /// Drafter ctx rows currently ingested (`None` on the native arm).
4204 pub fn draft_kv_len(&self) -> Option<usize> {
4205 match &self.draft {
4206 Glm5DraftState::Dflash2 { kv, .. } => Some(kv.len),
4207 _ => None,
4208 }
4209 }
4210 /// GATE ACCESSOR (rig gate 15): the first `rows` rows of every drafter K and V plane,
4211 /// host-side, `row_floats` = n_kv * head_dim. `None` on the native arm.
4212 #[allow(clippy::type_complexity)]
4213 // allow: (k planes, v planes) is the natural shape for a bit-identity diff
4214 pub fn draft_kv_rows_host(
4215 &self,
4216 e: &Engine,
4217 rows: usize,
4218 row_floats: usize,
4219 ) -> Option<(Vec<Vec<f32>>, Vec<Vec<f32>>)> {
4220 let Glm5DraftState::Dflash2 { kv, .. } = &self.draft else {
4221 return None;
4222 };
4223 let n = rows * row_floats;
4224 let take = |planes: &[CudaSlice<f32>]| -> Option<Vec<Vec<f32>>> {
4225 planes
4226 .iter()
4227 .map(|p| e.dtoh_view(&p.slice(0..n)).ok())
4228 .collect()
4229 };
4230 Some((take(&kv.k)?, take(&kv.v)?))
4231 }
4232 /// Context capacity of the session's cache (the server's ContextFull guard).
4233 pub fn cache_max_ctx(&self) -> usize {
4234 self.max_ctx
4235 }
4236 /// Drain the first-token profile (doc on the field). `None` with the profile off or
4237 /// once drained — the worker prints exactly one line per request.
4238 pub fn take_first_token_prof(&mut self) -> Option<SpecFirstTokenProf> {
4239 self.prof.take()
4240 }
4241 /// Drain the prompt-boundary capture (doc on the field; the dspark
4242 /// `take_prefix_capture` twin — the worker publishes it against `cache_ref`).
4243 pub fn take_prefix_capture(&mut self) -> Option<crate::spec::SpecBoundaryCapture> {
4244 self.prefix_capture.take()
4245 }
4246 /// True when the deferred prefix capture can publish NOW: a capture exists AND the
4247 /// DFlash2 drafter KV already covers the boundary. glm5 defers the prompt's feature
4248 /// ingest to round 1 (unlike dspark's at-creation ingest), so a drain that fired before
4249 /// the first burst would export an empty tail and waste the capture — the worker's
4250 /// sweep polls this instead.
4251 pub fn prefix_capture_ready(&self) -> bool {
4252 self.prefix_capture
4253 .as_ref()
4254 .is_some_and(|c| match &self.draft {
4255 Glm5DraftState::Dflash2 { kv, .. } => kv.len >= c.pos,
4256 _ => false,
4257 })
4258 }
4259 /// The drafter's readable KV tail at `upto` rows (the dspark `export_tail` seam) —
4260 /// `None` on the native arm or when the tail cannot cover the drafter window.
4261 pub fn export_draft_tail(
4262 &self,
4263 e: &Engine,
4264 upto: usize,
4265 ) -> Option<crate::dflash::DflashKvTail> {
4266 match &self.draft {
4267 Glm5DraftState::Dflash2 { kv, .. } => kv.export_tail(e, upto),
4268 _ => None,
4269 }
4270 }
4271 /// The session's trunk cache, for the worker's deferred prefix publication (the
4272 /// append-only-below-boundary slices) — never for mutation.
4273 pub fn cache_ref(&self) -> &Cache {
4274 &self.cache
4275 }
4276 /// Trunk rows currently committed (== `committed.len()` at burst boundaries).
4277 pub fn pos(&self) -> usize {
4278 self.cache.pos
4279 }
4280 /// EOS committed or the context guard tripped: the next burst would emit nothing.
4281 pub fn finished(&self) -> bool {
4282 self.done
4283 }
4284 /// True when the session is a legal demotion source (loop-port fold-in, map #8):
4285 /// GREEDY, UNPENALIZED only. A sampled session's committed stream depends on its
4286 /// session-owned Philox counters, and the plain batched sampler is a different random
4287 /// program mid-request (the exact exclusion the MTP and dspark sweeps carry). A
4288 /// penalized session (`pen: Some`, lane/spec-exclusions-20260902) is excluded the same
4289 /// way: the demotion flush (`glm5_spec_into_demoted`) argmaxes the boundary row RAW —
4290 /// no penalty pass — so demoting one would silently drop the request's penalties for
4291 /// that token, the exact failure class `glm5_penalty_admit`'s refusal exists to
4292 /// prevent, and dspark's structural reason for keeping penalized-greedy off spec
4293 /// entirely (revuto finding on the re-land, 2026-09-03). Penalized sessions therefore
4294 /// stay on spec until they end, like sampled ones.
4295 pub fn demote_eligible(&self) -> bool {
4296 self.sampling.is_none() && self.pen.is_none()
4297 }
4298}
4299
4300impl HybridModel {
4301 /// ONE-WAY DEMOTION HANDOFF for the glm5 session (loop-port fold-in — the map's #8,
4302 /// the `SpecSession::into_demoted` / `DsparkSpecSession::into_demoted` twin): consume
4303 /// the session and hand `(cache, next_pred)` to the plain batched-decode path, so a
4304 /// spec session admitted on a quiet box stops serializing the tick when load arrives
4305 /// (the spec-gate HIGH sweep's ship-safety lever; dspark receipt: "c=8 429.6 = parity
4306 /// (pre-lane -37%)").
4307 ///
4308 /// THE ANCHOR IS THE CARRIED-PENDING SHAPE: glm5 emits each round's bonus immediately
4309 /// (`round_tokens` include it) while the trunk consumes it only as the NEXT round's
4310 /// row 0 — so at every burst boundary the session holds ONE emitted-but-uncommitted
4311 /// token. Handing the cache over as-is would leave it one row short of the public
4312 /// stream, and `device_next` re-emitting the anchor would duplicate a served token.
4313 /// The flush below is `spec_flush_pending`'s exact analogue: ONE plain T=1 decode
4314 /// step commits the anchor (byte-identical to the never-drafted chain — the
4315 /// tparallel gate's accept-j-then-continue identity IS this claim), and its argmax
4316 /// becomes the handoff's `next_pred` — a token the batched path emits and feeds
4317 /// exactly as it would its own. One trunk pass, once per demotion, never per burst.
4318 ///
4319 /// ONE-WAY BY DESIGN: the draft state (MTP pending pairs / DFlash2 drafter KV and
4320 /// feature rows) and the Philox counters are DROPPED, freeing their VRAM; there is
4321 /// no cheap symmetric re-promotion (the spec.rs law, verbatim). Sampled sessions
4322 /// refuse loudly (`demote_eligible`; the worker's sweep excludes them first).
4323 pub fn glm5_spec_into_demoted(
4324 &self,
4325 e: &Engine,
4326 mut sess: Glm5SpecSession,
4327 ) -> Res<(Cache, u32)> {
4328 if !sess.demote_eligible() {
4329 let why = if sess.sampling.is_some() {
4330 "sampled sessions stay on spec until they end (session-owned Philox vs the \
4331 worker sampler is an unmeasured distributional seam — the MTP sweep's \
4332 exclusion, verbatim)"
4333 } else {
4334 "penalized sessions stay on spec until they end (this flush's plain argmax \
4335 carries no penalty pass; demoting would silently drop the request's \
4336 penalties for that token, lane/spec-exclusions-20260902)"
4337 };
4338 return Err(format!("glm5 demote: {why}").into());
4339 }
4340 if sess.cache.pos + 1 > sess.max_ctx {
4341 return Err(format!(
4342 "glm5 demote: no room to flush the live anchor ({} + 1 > ctx {})",
4343 sess.cache.pos, sess.max_ctx
4344 )
4345 .into());
4346 }
4347 let logits = self.decode_step(e, sess.anchor, &mut sess.cache)?;
4348 sess.committed.push(sess.anchor);
4349 let next = argmax(&logits) as u32;
4350 Ok((sess.cache, next))
4351 }
4352}
4353
4354/// Buckets of one eager drafter ingest (`glm5_dflash_ingest_rows`), written into the
4355/// first-token profile under the arm name that ran it.
4356#[derive(Default, Debug, Clone, Copy)]
4357struct DraftIngestStats {
4358 h2d_ms: f64,
4359 feat_ms: f64,
4360 kv_ms: f64,
4361 rows: usize,
4362 chunks: usize,
4363}
4364
4365impl DraftIngestStats {
4366 fn write(&self, pf: &mut SpecFirstTokenProf, arm: &'static str) {
4367 pf.draft_prime_ms = self.h2d_ms + self.feat_ms + self.kv_ms;
4368 pf.draft_prime_h2d_ms = self.h2d_ms;
4369 pf.draft_prime_feat_ms = self.feat_ms;
4370 pf.draft_prime_kv_ms = self.kv_ms;
4371 pf.draft_prime_rows = self.rows;
4372 pf.draft_prime_chunks = self.chunks;
4373 pf.draft_prime_arm = arm;
4374 }
4375}
4376
4377/// Gate instruments for `generate_spec_glm5_gated`. Documented as instruments: no serving
4378/// path constructs a non-default value.
4379#[derive(Default)]
4380pub struct Glm5SpecKnobs<'a> {
4381 /// `(round, draft_index, greedy_draft) -> draft` — deterministic forced-accept /
4382 /// forced-reject rounds for the end-to-end gate.
4383 pub draft_override: Option<&'a mut dyn FnMut(usize, usize, u32) -> u32>,
4384 /// RED ARM ONLY: skip the state rollback (pos still moves). A corrupted draft must then
4385 /// leave post-row-K KDA state and un-truncated latent rows behind — the end-to-end gate
4386 /// asserts the tape DIVERGES from plain decode (or the kpool residency tripwire fires).
4387 pub disable_rollback: bool,
4388 /// RED ARM ONLY: with an FR-Spec trim loaded, use the draft argmax RANK id as the vocab
4389 /// id (the q38 skipped-remap defect: 0/248 acceptance with every exactness gate green).
4390 /// The gate asserts the drafted sequence diverges from the untrimmed arm's while the
4391 /// output tape STAYS byte-identical to plain decode — the silent failure made loud.
4392 pub skip_d2t_remap: bool,
4393 /// GATE INSTRUMENT for the confidence gate (loop-port 2): `Some((p_min, pmin0))`
4394 /// overrides the `MEMRA_SPEC_PMIN`/`MEMRA_SPEC_PMIN0` env pair for this call — the
4395 /// env statics latch once per process, so the byte-identity gate drives its PMIN
4396 /// arms through here instead of the environment. `None` = the serving resolution.
4397 pub pmin_override: Option<(f32, bool)>,
4398 /// GATE INSTRUMENT (lane/glm5-accrace): trace every GREEDY round's accept decision to
4399 /// stderr as one `[glm5-accrace]` line — round, t, j, the drafts, the DEVICE argmax
4400 /// row (`argmax_token_device_col` + the one u32 readback the accept walk consumes), a
4401 /// HOST argmax over the same `vlogits` buffer, and a per-row (argmax, FNV-1a hash of
4402 /// the row's f32 bits) census.
4403 ///
4404 /// TWO THINGS IT SEPARATES, which is why it exists: (a) `dev != host` means the device
4405 /// accept path published a value the logits buffer does not justify (a readback/scratch
4406 /// race); (b) `dev == host` with a row hash that moves between two runs of the same
4407 /// deterministic fixture means the verify logits themselves were computed over
4408 /// corrupted state (an upstream walk/rollback race). The host read is issued AFTER the
4409 /// device path's own `dtoh_u32` has already synchronized the consuming stream, so the
4410 /// probe can only observe the race, never mask it.
4411 ///
4412 /// Never a serving surface: no serving path constructs a non-default value.
4413 pub accept_probe: bool,
4414}
4415
4416#[cfg(test)]
4417mod conf_keep_tests {
4418 use super::glm5_conf_keep;
4419
4420 /// The spec.rs chain-break semantics, pinned CPU-side (loop-port 2): break at the
4421 /// first sub-threshold slot; slot 0 survives a miss unless PMIN0.
4422 #[test]
4423 fn conf_keep_matches_the_spec_rs_break_semantics() {
4424 // Gate off: everything kept.
4425 assert_eq!(glm5_conf_keep(&[0.1, 0.1], 0.0, true), 2);
4426 // All confident: everything kept.
4427 assert_eq!(glm5_conf_keep(&[0.9, 0.8, 0.7], 0.5, false), 3);
4428 // Break mid-chain at the first miss; the confident tail after it never rides
4429 // (prefix truncation — the accept rule could never commit past the gap anyway).
4430 assert_eq!(glm5_conf_keep(&[0.9, 0.2, 0.9], 0.5, false), 1);
4431 assert_eq!(glm5_conf_keep(&[0.9, 0.2, 0.9], 0.5, true), 1);
4432 // Slot-0 miss: survives without PMIN0 (the j > 0 arm of the break condition), and
4433 // does NOT latch — slot 1 is judged on its own confidence (the spec.rs chain
4434 // evaluates each slot's p independently)...
4435 assert_eq!(glm5_conf_keep(&[0.2, 0.9], 0.5, false), 2);
4436 // ...but a sub-threshold slot past 0 still breaks.
4437 assert_eq!(glm5_conf_keep(&[0.2, 0.2], 0.5, false), 1);
4438 // PMIN0 arms the zero-draft round.
4439 assert_eq!(glm5_conf_keep(&[0.2, 0.9], 0.5, true), 0);
4440 // Boundary: q == p_min is NOT below it (strict <, the spec.rs test).
4441 assert_eq!(glm5_conf_keep(&[0.5, 0.5], 0.5, true), 2);
4442 // Empty chain: nothing to keep.
4443 assert_eq!(glm5_conf_keep(&[], 0.5, true), 0);
4444 }
4445}