spark_model/model/
impl_a1.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3#![allow(unused_imports, dead_code)]
4
5use parking_lot::Mutex;
6use std::collections::HashMap;
7use std::sync::Arc;
8
9use anyhow::{Result, bail};
10use avarok_core::config::{LayerType, ModelConfig};
11use spark_runtime::buffers::BufferArena;
12use spark_runtime::gpu::{DevicePtr, GpuBackend, GraphHandle, KernelHandle};
13use spark_runtime::kv_cache::PagedKvCache;
14
15use super::block_mgmt::{
16    apply_evicted_blocks, ensure_blocks_through_decode, ensure_blocks_through_prefill,
17    extract_layer_refs, reuse_prefix_match_disk_ids,
18};
19use super::ssm_pool::SsmStatePool;
20use super::ssm_snapshot::SsmSnapshotPool;
21use super::types::{PinnedMetaStaging, TransformerModel};
22use crate::layer::{
23    AttnMetadataDev, ForwardContext, GdnPrefillBuffers, LayerState, SsmLayerState, TransformerLayer,
24};
25use crate::layers::ops;
26use crate::speculative::DraftProposer;
27use crate::traits::{ChunkedPrefillPageMetadata, Model, SequenceState};
28use crate::weight_map::{DenseWeight, MtpWeights, QuantizedWeight};
29
30/// lm_head tile-GEMM decode path: **ON by default**, disabled by
31/// `AVAROK_NO_LMHEAD_TGEMM=1`. Evaluated ONCE at construction — the switch
32/// decides whether the transposed twin is built at all, so setting it later has
33/// no effect. Presence-style check (`AVAROK_*=0` is NOT "off").
34///
35/// Measured C=16: 113.10 -> 119.32 tok/s (+5.50%, disjoint ranges, 4 reps).
36/// `padded_n <= 4` is untouched and stays byte-identical, so C=1 is unaffected.
37/// The twin costs ~681 MB and leaves the KV pool at 4759 blocks vs 4757 without
38/// it — no measurable KV impact.
39fn lmhead_tgemm_enabled() -> bool {
40    std::env::var("AVAROK_NO_LMHEAD_TGEMM").ok().as_deref() != Some("1")
41}
42
43impl TransformerModel {
44    pub fn new(
45        config: ModelConfig,
46        embed_tokens: DenseWeight,
47        final_norm: DenseWeight,
48        lm_head_weight: DenseWeight,
49        lm_head_nvfp4: Option<QuantizedWeight>,
50        // Runtime FP8 LM head (`--lm-head-dtype fp8`). Mutually exclusive with
51        // `lm_head_nvfp4`; `None` for the NVFP4/BF16/default paths (byte-identical).
52        lm_head_fp8: Option<crate::weight_map::Fp8DenseWeight>,
53        // Separate NVFP4 head used ONLY by the MTP draft proposer when the
54        // main head is kept BF16 (`skip_lm_head_quantization()`). `None` for
55        // the NVFP4-main default, in which case the proposer falls back to
56        // `lm_head_nvfp4`. Drafts are always verified by the main BF16 head,
57        // so this approximate head never affects an accepted token.
58        mtp_lm_head_nvfp4: Option<QuantizedWeight>,
59        layers: Vec<Box<dyn TransformerLayer>>,
60        buffers: BufferArena,
61        kv_cache: PagedKvCache,
62        mtp_weights: Vec<MtpWeights>,
63        gpu: Box<dyn GpuBackend>,
64        max_seq_len: usize,
65        max_batch_size: usize,
66        mtp_quant: crate::layers::MtpQuantization,
67        use_speculative: bool,
68        prefix_cache: Box<dyn spark_runtime::prefix_cache::PrefixCache>,
69        mtp_vocab_size: u32,
70        comm: Option<std::sync::Arc<dyn spark_comm::CommBackend>>,
71        self_speculative: bool,
72        num_drafts: usize,
73        vision_encoder: Option<crate::layers::VisionTower>,
74        ssm_cache_slots: usize,
75        ssm_checkpoint_interval: usize,
76    ) -> Result<Self> {
77        // `rms_norm_kernel` normalizes exactly one weight: `final_norm` (a
78        // checkpoint tensor). Models that ship HF-vanilla norm weights load it
79        // exactly and must use the vanilla kernel.
80        let rms_norm_kernel = if crate::ships_vanilla_norm_weights(&config) {
81            gpu.kernel("rms_norm_vanilla", "rms_norm_vanilla")?
82        } else {
83            gpu.kernel("norm", "rms_norm")?
84        };
85        let dense_gemv_kernel = gpu.kernel("gemv", "dense_gemv_bf16")?;
86        // FP32-output dense GEMV — the FP32 logits path required an FP32
87        // residual stream, which no longer exists, so this stays
88        // KernelHandle(0) and the BF16 path is always taken.
89        let dense_gemv_fp32out_kernel = KernelHandle(0);
90        let w4a16_gemv_kernel = gpu.kernel("w4a16_gemv", "w4a16_gemv")?;
91        let w4a16_gemv_logits_kernel = gpu.kernel("w4a16_gemv", "w4a16_gemv_logits")?;
92        // lm_head shares the tile GEMM, so route it through the same resolver as
93        // the SSM/attention sites — it picks the 3-deep pipeline variant when
94        // present. lm_head launches 1938 CTAs and already sits at ~83% of
95        // achievable, so the expected gain here is small; measured, not assumed.
96        let w4a16_gemm_t_kernel = if crate::layers::tgemm_probe_ok(&config.model_type) {
97            crate::layers::tgemm_kernel(gpu.as_ref())
98        } else {
99            KernelHandle(0)
100        };
101        // Lossless BF16-MMA sibling for lm_head, OPT-IN via AVAROK_LMHEAD_LOSSLESS=1.
102        // Measured cost 1.81% at C=16 (129.68 -> 127.33). Default is the faster
103        // FP8-activation path because the accuracy question it addresses CANNOT
104        // BE MEASURED until vLLM parity lifts the BFCL embargo — and the 1.81%
105        // is throughput needed to REACH parity. The risk is real but indirect:
106        // the bf16-floor finding was superseded on the WEIGHT axis, and this is
107        // the ACTIVATION axis, which was never examined. Re-decide at parity.
108        let w4a16_gemm_t_bf16_kernel = if std::env::var("AVAROK_LMHEAD_LOSSLESS").is_ok() {
109            crate::layers::try_kernel(gpu.as_ref(), "w4a16", "w4a16_gemm_t_m128_bf16_v2")
110        } else {
111            spark_runtime::gpu::KernelHandle(0)
112        };
113        let w4a16_gemm_kernel = gpu.kernel("w4a16", "w4a16_gemm")?;
114        let w4a16_gemv_batch2_kernel = gpu.kernel("w4a16_gemv", "w4a16_gemv_batch2")?;
115        // Narrow batched-GEMV family (M=4..8) for the K=3..8 verify lm_head
116        // (try_kernel per tier: 0-handle on targets that predate a tier;
117        // dispatch widens, then falls back to the GEMM).
118        let w4a16_batchm = crate::layers::w4a16_gemv_tiers::W4a16BatchmTiers::resolve(gpu.as_ref());
119        // M<=16 batched GEMV for the wide BATCHED-DECODE lm_head. The SSM mixer
120        // already carries this handle (qwen3_ssm/mod.rs); the model level did
121        // not, so the decode head had no arm above 8 and fell to the M64-tile
122        // GEMM. Same try_kernel contract: 0-handle -> dispatch falls back.
123        let w4a16_gemv_batch16_kernel =
124            crate::layers::try_kernel(gpu.as_ref(), "w4a16_gemv", "w4a16_gemv_batch16");
125        // FP8 E4M3 LUT GEMV for the `--lm-head-dtype fp8` head. Loaded
126        // unconditionally (a handle is cheap); only invoked when `lm_head_fp8`
127        // is set, so the NVFP4/BF16 paths never touch it.
128        let dense_gemv_fp8w_kernel = gpu.kernel("gemv_fp8w", "dense_gemv_fp8w")?;
129        // FP8 dual-GEMV (batch=2): present on images that ship the kernel;
130        // try_kernel keeps the handle 0 on older sets so dispatch falls back
131        // to the per-token loop.
132        let dense_gemv_fp8w_batch2_kernel = crate::layers::try_kernel(
133            gpu.as_ref(),
134            "dense_gemv_fp8w_batch2",
135            "dense_gemv_fp8w_batch2",
136        );
137        let dense_gemm_kernel = gpu.kernel("gemm", "dense_gemm_bf16")?;
138        let dense_gemv_batchm_kernel = gpu
139            .kernel("dense_gemv_bf16_batchm", "dense_gemv_bf16_batchm")
140            .unwrap_or(spark_runtime::gpu::KernelHandle(0));
141        // Tensor-core BF16 head arm (#927/#928). Optional: a target whose tree
142        // does not carry `dense_gemm_m16_bf16.cu` (Hopper owns it) never looks
143        // it up, and a 0 handle is exactly how `lm_head_m16_tc_route`
144        // declines. Where the module is compiled, a handle is cheap and
145        // `AVAROK_LM_HEAD_M16_TC` decides whether it is ever launched.
146        let lm_head_m16_tc_kernel = crate::layers::try_target_kernel(
147            gpu.as_ref(),
148            "dense_gemm_m16_bf16",
149            "dense_gemm_m16_bf16",
150        );
151        let lm_head_m16_tc_n64_kernel = crate::layers::try_target_kernel(
152            gpu.as_ref(),
153            "dense_gemm_m16_bf16",
154            "dense_gemm_m16_bf16_n64",
155        );
156        let argmax_kernel = gpu.kernel("argmax", "argmax_bf16")?;
157        let argmax_batch_kernel = gpu
158            .kernel("argmax", "argmax_bf16_batch")
159            .unwrap_or(spark_runtime::gpu::KernelHandle(0));
160        let argmax_logits_kernel = gpu.kernel("argmax", "argmax_fp32")?;
161        let batched_embed_kernel = gpu.kernel("embed_from_argmax", "batched_embed")?;
162        let fill_slots_kernel = gpu.kernel("metadata_fill", "fill_slots_from_block_table")?;
163        let profile = config.profile;
164        let profile_first = std::env::var("AVAROK_PROFILE_FIRST").is_ok();
165
166        // Pin the split-K attention split count to the configured max batch so
167        // a sequence's attention reduction is invariant to how many other
168        // sequences are co-batched (concurrent-decode determinism — see
169        // tasks/determinism_investigation.md).
170        // ★ A FRESH RESOLVE, DELIBERATELY — not `*ModelLevers::get()`.
171        // `speculative::shadow_topk` documents the rule: the levers resolve
172        // "once per run rather than caching the answer in a `OnceLock` that a
173        // swap would pin". Building a second model must re-resolve, so this
174        // path stays uncached. It runs once per model, so it costs nothing;
175        // `get()` exists for the read-only sites that used to call this per
176        // layer per prefill.
177        let mut levers = ops::ModelLevers::from_env();
178        levers.max_decode_seqs = (max_batch_size as u32).max(1);
179
180        tracing::info!(
181            "TransformerModel: {} layers, vocab={}, hidden={}{}{}",
182            layers.len(),
183            config.vocab_size,
184            config.hidden_size,
185            if profile { " [PROFILE MODE]" } else { "" },
186            if profile_first {
187                " [PROFILE_FIRST]"
188            } else {
189                ""
190            },
191        );
192
193        // Build SSM state pool (with MTP intermediate/checkpoint pools only if speculative decoding enabled)
194        // num_intermediates = K, the verify-width ceiling. The CONV pools
195        // allocate K snapshots per slot; the H pools allocate K-1 (index
196        // K-1 is never written or read — see ssm_reserve) and tier by slot.
197        // For MTP K=2/3/4 verify: K = num_drafts + 1.
198        // For DFlash K=γ verify: K = γ + 1 (drafter's γ drafts + 1 verified bonus slot).
199        // Pool size = max of both so DFlash and MTP can coexist on the same model.
200        let dflash_kgamma = if !config.dflash_capture_layers.is_empty() {
201            // The +1 is the prefix bonus position in the verify input
202            // `[last_token, draft_0, ..., draft_{γ-1}]`. Sized from the
203            // RESOLVED drafter γ (factory sets `config.dflash_gamma` from the
204            // drafter checkpoint / --dflash-gamma): the legacy 17-wide
205            // ceiling (γ=16-era) cost ~1.5 GB of intermediates per slot per
206            // GB at γ=8 — ~12 GB across 8 slots on qwen3.8-27B — for slots
207            // the verify never touches (2026-08-19 256K/C8 boot ledger).
208            // Unknown γ keeps the 17-wide fallback.
209            config.dflash_gamma.map(|g| g + 1).unwrap_or(17)
210        } else {
211            0
212        };
213        // DFlash needs the SSM verify pools regardless of MTP weight presence
214        // or lm_head quantization — its K=γ verify path checkpoints SSM state
215        // for partial-accept rollback. Force `has_mtp` on whenever DFlash is
216        // active so the checkpoint pools exist.
217        // The MTP proposer needs an NVFP4 vocab head for drafting: either the
218        // main head (NVFP4 default) or the draft-only head built when the main
219        // head is BF16. `draft_lm_head_nvfp4` resolves to whichever is present.
220        let draft_lm_head_nvfp4 = mtp_lm_head_nvfp4.or(lm_head_nvfp4);
221        // 🔴 This flag SIZES THE RECURRENT ROLLBACK POOLS (checkpoints + per-token
222        // intermediates). It has to be true for every proposer that can reject a draft, not
223        // just the Qwen-shaped one.
224        //
225        // 🪤 GLM-5.3 populates NEITHER of the first two signals: its MTP block is
226        // `layers.{num_hidden_layers}`, not the Qwen `MtpWeights`, and its LM head is BF16 so
227        // there is no NVFP4 draft head. Its proposer is installed AFTER construction via
228        // `set_dflash_proposer`, so `new()` cannot see it either. `mtp_layer_types` is what the
229        // config parser records when the CHECKPOINT declares MTP layers — the one signal
230        // available this early. Without it the pools are never allocated and the first decode
231        // panics in `ssm_pool::h_checkpoint` ("len is 0 but the index is 0").
232        let checkpoint_declares_mtp = !config.mtp_layer_types.is_empty();
233        let has_mtp = self_speculative
234            || (use_speculative
235                && ((!mtp_weights.is_empty() && draft_lm_head_nvfp4.is_some())
236                    || checkpoint_declares_mtp))
237            || dflash_kgamma > 0;
238        let num_intermediates = if !has_mtp {
239            0
240        } else if dflash_kgamma > 0 {
241            // DFlash serve: the block drafter OWNS the verify path, so the
242            // widest verify is K = gamma + 1 and `num_drafts` never reaches
243            // the pool. Taking max(num_drafts+1, kgamma) sized these pools for
244            // the MTP K=2/3/4 ladder that a DFlash serve never runs: at
245            // num_drafts=15 that is 16 wide against a real ceiling of 9, and
246            // these pools are the single largest non-weight allocation on the
247            // box — 21.9 GB at 8 slots x 48 layers, as large as the model
248            // itself. Sizing to the real ceiling reclaims ~9.6 GB
249            // (2026-08-19 128K/C8 boot ledger; observed verify widths were
250            // K=8 and ks=[8;n], never above).
251            //
252            // The K2/K3/K4 arms still reachable on a DFlash serve (when the
253            // drafter returns <4 drafts) verify at K<=4, comfortably inside
254            // this. `require_verify_rollback_supported` remains the backstop
255            // if a future path asks for more.
256            dflash_kgamma
257        } else {
258            num_drafts + 1
259        };
260        let ssm_pool = std::sync::Arc::new(SsmStatePool::new(
261            &config,
262            max_batch_size,
263            has_mtp,
264            num_intermediates,
265            num_drafts,
266            // Stage-3 f16-SIZED h pools. No CLI surface publishes this and
267            // preflight refuses it until prefill narrowing lands, so it is
268            // false on every serveable config today.
269            crate::layers::qwen3_ssm::ssm_h_f16_pool_enabled(),
270            // `--ssm-rollback-mode` (EXPERIMENTAL replay scaffold; default
271            // snapshot, published by spark-server's serve_flags).
272            crate::ssm_reserve::ssm_rollback_mode(),
273            gpu.as_ref(),
274        )?);
275
276        // Fail fast if an SSM tier was requested (`AVAROK_SSM_TIER`) on a model
277        // with no recurrent state — a tier request there was previously a
278        // silent no-op. No-op when the tier is unset (default path).
279        super::ssm_tier::ensure_ssm_tier_capability(&config)?;
280
281        // SSM snapshot pool: Marconi prefix-cache slots + Phase-C
282        // decode-rollback ring. The decode-rollback region is only sized
283        // for SSM models — `num_ssm_layers == 0` makes both regions
284        // collapse to empty. The ring retains DECODE_ROLLBACK_RING_SLOTS
285        // boundary snapshots per sequence (DECOUPLED from ROLLBACK_RESTEER_CAP:
286        // the cap bounds re-steer attempts, the ring must retain enough
287        // boundaries that a clean PRE-loop one survives — `CAP+1=3` was too
288        // small and forced NoSsmSnapshot declines). Sized for every
289        // active-sequence pool slot (`max_batch_size`).
290        // The ring's ONLY writer (scheduler snapshot_boundary_if_ssm) and
291        // reader (content-loop rollback_to_boundary) live on the PLAIN decode
292        // path — the speculative path does its rejection rollback through the
293        // verify snapshot, never this ring. Under `--speculative` the ring is
294        // therefore unreachable, and on this model it is NOT cheap: 8 slots x
295        // max_batch x the full SSM blob (27B: 158.9 MB) = ~19.9 GB at batch 16,
296        // allocated up front. Skip it when speculative decode is on.
297        // The ring-depth decision (the published `--ssm-decode-ring-slots` /
298        // #915 auto-fit depth, env overrides, speculative/watchdog skip) is
299        // SSOT'd in `crate::ssm_reserve::decode_rollback_ring_slots`
300        // — spark-server's `preflight_reserve` calls the SAME helper, so the
301        // GPU reservation and this allocation cannot drift. The scheduler
302        // keys off `decode_rollback_ring_slots()`, so a 0 here disables save
303        // AND rollback coherently (rollback declines, the documented
304        // fail-open).
305        let ring = crate::ssm_reserve::decode_rollback_ring_slots(
306            ssm_pool.num_ssm_layers,
307            use_speculative,
308        );
309        if let Some(reason) = ring.skip_reason {
310            let per_seq = (ssm_pool.h_bytes + ssm_pool.conv_bytes)
311                * ssm_pool.num_ssm_layers
312                * avarok_kernels::DECODE_ROLLBACK_RING_SLOTS;
313            tracing::info!(
314                "SSM decode-rollback ring: SKIPPED ({}) — the ring's save/rollback \
315                 path only runs on plain decode with watchdogs enabled. Saves {:.1} GB \
316                 ({} seqs x {} slots x full SSM blob). If plain-decode loop re-steer is \
317                 ever reached it fail-opens to decline; AVAROK_SSM_DECODE_RING=1 \
318                 force-restores the ring.",
319                reason,
320                (per_seq * max_batch_size) as f64 / 1e9,
321                max_batch_size,
322                avarok_kernels::DECODE_ROLLBACK_RING_SLOTS,
323            );
324        }
325        let decode_ring_slots = ring.slots;
326        // Marconi snapshot region (2380 MiB on GLM-5.3 at 16 slots). SSOT:
327        // `ssm_reserve::marconi_snapshot_slots` makes the SAME decision
328        // `preflight_reserve` made before the weights loaded. The region's
329        // only reader is a prefix-cache lookup, so an inactive cache makes
330        // every slot unreachable for the life of the process. Asking the
331        // constructed cache (`is_active`) rather than the CLI flag also
332        // covers the compressed-DeepSeek-V4 downgrade, where the flag is set
333        // but `NoPrefixCaching` is what actually gets installed.
334        let marconi =
335            crate::ssm_reserve::marconi_snapshot_slots(ssm_cache_slots, prefix_cache.is_active());
336        if let Some(reason) = marconi.skip_reason {
337            tracing::info!(
338                "SSM snapshot pool: Marconi region SKIPPED ({}) — {} slot(s) x {} layer(s) \
339                 = {:.0} MB freed for KV (restore with --enable-prefix-caching, or \
340                 AVAROK_SSM_MARCONI_FULL to allocate anyway)",
341                reason,
342                ssm_cache_slots,
343                ssm_pool.num_ssm_layers,
344                (ssm_cache_slots
345                    * ssm_pool.num_ssm_layers
346                    * (ssm_pool.h_bytes + ssm_pool.conv_bytes)) as f64
347                    / (1024.0 * 1024.0),
348            );
349        }
350        let ssm_cache_slots = marconi.slots;
351        let ssm_snapshots = SsmSnapshotPool::new(
352            ssm_cache_slots,
353            ssm_pool.h_bytes,
354            ssm_pool.conv_bytes,
355            ssm_pool.num_ssm_layers,
356            decode_ring_slots,
357            max_batch_size,
358            // Last-token hidden snapshot: post-final-norm `norm_output` is
359            // BF16 (`hidden_size` elements). Used to emit exact-hit logits
360            // without re-running the last token through the SSM layers.
361            config.hidden_size * 2,
362            gpu.as_ref(),
363        )?;
364        // Optional SSM snapshot spill tier. `None` (default) keeps the reclaim
365        // drop path byte-identical; blob sizing tracks the pool's spill layout.
366        let ssm_tier_store = super::impl_a1_init::build_ssm_tier_store(
367            &config,
368            ssm_snapshots.spill_blob_bytes(),
369            ssm_pool.num_ssm_layers,
370        )?;
371        if ssm_checkpoint_interval > 0 && ssm_cache_slots > 0 {
372            tracing::info!(
373                "Marconi intermediate checkpoints: every {} blocks ({} tokens at block_size={})",
374                ssm_checkpoint_interval,
375                ssm_checkpoint_interval * kv_cache.block_size(),
376                kv_cache.block_size(),
377            );
378        }
379
380        // Fixed metadata stride for CUDA graph compatibility
381        let max_blocks_per_seq = (max_seq_len / kv_cache.block_size() + 1) as u32;
382
383        // Permanent dummy KV block for padding sequences. Must be explicitly
384        // zeroed: `gpu.alloc()` returns uninitialized memory, and any kernel
385        // OOB-read (now routed here via the sentinel block_table_flat default
386        // fill in upload_batch_metadata_*) would otherwise dequant random
387        // bytes and inject garbage into attention scores.
388        let mut kv_cache = kv_cache;
389        let dummy_kv_block = kv_cache.alloc_block()?;
390        kv_cache.zero_block(dummy_kv_block, gpu.as_ref(), gpu.default_stream())?;
391        gpu.synchronize(gpu.default_stream())?;
392
393        // Transposed lm_head twin, PADDED so the tile GEMM's 16-byte cp.async B
394        // loads are aligned. The stride must be a multiple of 16; 128 also keeps
395        // whole N-tiles. Without the pad, N = vocab = 248077 (ODD) misaligns 15 of
396        // every 16 k-rows => the campaign's long-standing sticky CUDA 716.
397        // Default ON (kill: AVAROK_NO_LMHEAD_TGEMM=1). KV impact is nil: the pool
398        // reads 4759 blocks with the twin vs 4757 without. See STATE.md.
399        let lm_head_nvfp4_t = match (&lm_head_nvfp4, lmhead_tgemm_enabled()) {
400            (Some(w), true) => {
401                let (t, stride) =
402                    crate::weight_map::QuantizedWeight::transpose_concat_for_gemm_padded(
403                        gpu.as_ref(),
404                        &[(w, config.vocab_size)],
405                        config.hidden_size,
406                        16,
407                        128,
408                    )?;
409                // A padded stride is only safe on targets whose `w4a16_gemm_t`
410                // actually takes `ldb`. Every served vocab except this one is a
411                // multiple of 128 (stride == vocab, so `ldb` is a no-op and any
412                // kernel is fine); when it is NOT, a kernel missing the parameter
413                // strides by N and shears every row past the first — silently, on
414                // architectures that tolerate the misalignment. Say so loudly.
415                if stride != config.vocab_size {
416                    tracing::warn!(
417                        "lm_head twin uses a PADDED stride ({} != vocab {}): this target's \
418                         w4a16_gemm_t MUST accept the `ldb` argument, or decode at padded_n>=5 \
419                         will read sheared rows. Disable with AVAROK_NO_LMHEAD_TGEMM=1.",
420                        stride,
421                        config.vocab_size
422                    );
423                }
424                tracing::info!(
425                    "lm_head transposed twin: vocab={} -> padded stride={} (vocab%16={}), tile GEMM active",
426                    config.vocab_size,
427                    stride,
428                    config.vocab_size % 16
429                );
430                Some((t, stride as u32))
431            }
432            _ => None,
433        };
434        // Drafter-side view of the twin: valid ONLY when the drafter head IS
435        // the shared main head (`mtp_lm_head_nvfp4` absent) — the twin is a
436        // transpose of `lm_head_nvfp4` specifically, so handing it to a
437        // DEDICATED draft head would silently score drafts against the wrong
438        // weight. Zero extra memory in the shared case (aliases the twin).
439        let draft_lm_head_nvfp4_t = if mtp_lm_head_nvfp4.is_none() {
440            lm_head_nvfp4_t
441        } else {
442            None
443        };
444        // Build MTP proposer (extracted to keep `new` under the file cap).
445        let proposer: Option<Arc<dyn DraftProposer>> = super::impl_a1_init::build_mtp_proposer(
446            use_speculative,
447            mtp_weights,
448            embed_tokens,
449            draft_lm_head_nvfp4,
450            draft_lm_head_nvfp4_t,
451            &config,
452            gpu.as_ref(),
453            mtp_quant,
454            mtp_vocab_size,
455            max_seq_len,
456            kv_cache.num_blocks(),
457            &levers,
458        );
459
460        if self_speculative {
461            let num_ssm = config.num_ssm_layers();
462            let num_attn = config.num_attention_layers();
463            tracing::info!(
464                "Self-speculative decoding: ENABLED (skipping {} SSM layers, keeping {} attention layers)",
465                num_ssm,
466                num_attn,
467            );
468        }
469
470        // MTP hidden state save buffer (1 × hidden_size FP32)
471        let mtp_hidden_save = gpu.alloc(config.hidden_size * 4)?;
472        // Batched-verify hidden stash: [VERIFY_WY_TABLE_SEQS, hidden] BF16 —
473        // one slot per sequence of the widest batched verify chunk (n ≤ 32,
474        // the K-vs-batch ladder envelope, SSOT in `crate::layer`). Only
475        // meaningful with an MTP proposer — NULL otherwise (the batched
476        // verify path self-gates on it via can_batch_verify).
477        let verify_hidden_stash = if proposer.is_some() {
478            gpu.alloc(crate::layer::VERIFY_WY_TABLE_SEQS * config.hidden_size * 2)?
479        } else {
480            DevicePtr::NULL
481        };
482        // Batched-verify WY pointer-table staging (fixed address for CUDA
483        // graph stability; contents refreshed pre-graph every batched verify
484        // step). One [h|Hi0|Hi1|Hi2] x 4-entry slice per GDN layer — ~6 KB.
485        // NULL without an MTP proposer or on non-SSM models (path self-gates).
486        let verify_wy_tables = if proposer.is_some() && config.num_ssm_layers() > 0 {
487            let bytes = config.num_ssm_layers() * crate::layer::VERIFY_WY_LAYER_STRIDE_BYTES;
488            let buf = gpu.alloc(bytes)?;
489            gpu.memset(buf, 0, bytes)?;
490            buf
491        } else {
492            DevicePtr::NULL
493        };
494        let gdn_woa_na_tab = if verify_wy_tables.is_null() {
495            DevicePtr::NULL
496        } else {
497            let b = gpu.alloc(crate::layer::VERIFY_WY_TABLE_SEQS * 4)?;
498            gpu.memset(b, 0, crate::layer::VERIFY_WY_TABLE_SEQS * 4)?;
499            b
500        };
501        // Catch-up ring: 512 rows covers the gate's serial re-probe interval
502        // (256 tokens) with 2x margin; ~4 MB at hidden 4096. Only allocated
503        // when the staged feature is enabled.
504        let mtp_catchup_ring = if crate::speculative::mtp_catchup_enabled() {
505            gpu.alloc(super::types::MTP_CATCHUP_RING_ROWS * config.hidden_size * 2)?
506        } else {
507            DevicePtr::NULL
508        };
509
510        // Whole-prompt hidden capture buffer, [rows, hidden_size] BF16 —
511        // 335 MB at 32k/h=5120. Backs BOTH halves of the drafter-context
512        // feature (see `crate::model::drafter_context`); NULL here disables
513        // prefill AND carry, since the carry path reads this buffer.
514        //
515        // Three conditions, all necessary: MTP must be active, the feature must
516        // not be killed, and the head must be a precision the batched prefill
517        // can actually run at — an NVFP4/FP8 MTP head would allocate this and
518        // never write it.
519        //
520        // Two ceilings, both binding, one value. The proposer's reachable
521        // context (`DraftProposer::prefill_hidden_rows`, default `max_seq_len`):
522        // GLM-5.3's drafter is a DSA block capped at `max_dsa_context`, so at
523        // `--max-seq-len 524288` this buffer was 4.0 GiB of which all but
524        // 128 MiB was unreachable, and unreserved, because it is allocated
525        // after the KV pool is sized (ANOMALIES A59). And the DFlash ctx cap
526        // when DFlash is on: a DFlash serve above the 16K cap wrote past the
527        // allocation once a prompt crossed it (`cuMemcpyDtoDAsync` status 1,
528        // reported on #844 by rsafier). `capture_rows` is computed OUTSIDE the
529        // branch because it is also the buffer's row capacity
530        // (`mtp_prefill_capacity` below): the two numbers must be one value.
531        let mtp_prefill_rows = proposer
532            .as_ref()
533            .map_or(max_seq_len, |p| p.prefill_hidden_rows(max_seq_len))
534            .min(max_seq_len);
535        let capture_rows = if dflash_kgamma > 0 {
536            let cap = crate::layers::dflash_ctx_cap();
537            if cap == 0 {
538                mtp_prefill_rows
539            } else {
540                mtp_prefill_rows.min(cap)
541            }
542        } else {
543            mtp_prefill_rows
544        };
545        let mtp_prefill_hidden = if has_mtp
546            && mtp_quant.supports_drafter_prefill()
547            && crate::layers::mtp_drafter_prefill_enabled(&levers)
548        {
549            // Bound the capture to what the drafter can actually CONSUME.
550            // `prefill_drafter` writes into the drafter's own KV, which is
551            // capped at the DFlash ctx window, so a capture longer than that
552            // window is memory nothing can read: 1342 MB at --max-seq-len
553            // 131072 against a 16K window that can hold 168 MB of it.
554            // A prompt past the window simply does not get the whole-prompt
555            // drafter prefill (the coverage check at the consume site already
556            // handles that — blind beats poisoned); it costs acceptance on
557            // very long cold turns, not correctness.
558            //
559            // Pure-MTP serves keep the full ceiling: this narrowing is only
560            // sound because the DFlash drafter's own capacity is the binding
561            // constraint, and that reasoning does not transfer.
562            let bytes = capture_rows * config.hidden_size * 2;
563            tracing::info!(
564                "MTP drafter context: allocating {:.0} MB prompt-hidden capture \
565                 ({} x {} BF16){}",
566                bytes as f64 / 1e6,
567                capture_rows,
568                config.hidden_size,
569                if capture_rows < max_seq_len {
570                    format!(
571                        " — capped from --max-seq-len {max_seq_len} to the reachable \
572                         context (proposer ceiling A59 and/or the DFlash ctx cap)"
573                    )
574                } else {
575                    String::new()
576                },
577            );
578            gpu.alloc(bytes)?
579        } else {
580            if has_mtp
581                && !mtp_quant.supports_drafter_prefill()
582                && crate::layers::mtp_drafter_prefill_enabled(&levers)
583            {
584                tracing::info!(
585                    "MTP drafter context: INACTIVE — the batched drafter prefill \
586                     needs a BF16 MTP head (--mtp-quantization bf16); this head is \
587                     {mtp_quant:?}. No prompt-hidden capture allocated.",
588                );
589            }
590            DevicePtr::NULL
591        };
592
593        // DFlash 5-layer hidden-state stack. Allocated only when a
594        // BlockDiffusionDraftHead is the active proposer (`config.dflash_capture_layers`
595        // populated by the loader from the drafter's `dflash_config.target_layer_ids`).
596        // Size: N_capture × hidden_size × bf16 (typically 5 × 2048 × 2 = 20 KB).
597        let dflash_capture_layers: Vec<usize> = config.dflash_capture_layers.clone();
598        // Row capacity of the K-row capture buffer. KMAX = dflash_kgamma (=17 >=
599        // max verify K = gamma) so the K=gamma EAGLE path can capture every verify row;
600        // pre-fix paths use only rows 0-1. Stored on the model as the single
601        // source of truth so `try_dflash_capture_all` can bound its writes.
602        //
603        // Widened to `max_batch_size` K-row BANDS: a cross-sequence batched
604        // K=gamma verify (and batched decode) captures every (sequence, row)
605        // pair, sequence i writing band i at row `i * dflash_kgamma`. The
606        // scheduler reads a band back through `commit_ctx`'s `scratch_row`.
607        // Cost is trivial (8 seqs x 9 rows x 5 layers x 5120 x 2 B ~ 3.7 MB)
608        // and the single-sequence paths keep using band 0 unchanged.
609        let dflash_hidden_save_rows = if dflash_capture_layers.is_empty() {
610            0
611        } else {
612            dflash_kgamma.max(2) * max_batch_size.max(1)
613        };
614        let dflash_hidden_save = if dflash_capture_layers.is_empty() {
615            None
616        } else {
617            let n = dflash_capture_layers.len();
618            // Row-major K-row buffer: [row0 | row1 | ... | row_{KMAX-1}], each row =
619            // n_capture * hidden_size * bf16. Rows 0/1 keep their legacy offsets
620            // (0 and ctx_slot_bytes) so all K=2 readers (propose row 0,
621            // dflash_accept_append row 1) are unaffected.
622            Some(gpu.alloc(dflash_hidden_save_rows * n * config.hidden_size * 2)?)
623        };
624
625        // EP command buffer for token broadcast (4 bytes, u32)
626        let ep_cmd_buf = gpu.alloc(4)?;
627
628        // SOLID Incr-4: dedicated fixed-address buffer for the batched-decode MoE
629        // per-row fold map. max_batch_size i32 rows (e.g. 32·4 = 128 B). Allocated
630        // unconditionally like ep_cmd_buf/mtp_hidden_save — self.lora is populated
631        // post-construction (set_lora_weights), so we can't gate on it here, and
632        // the cost is negligible. Fixed address → graph-safe; contents copied per
633        // decode step. Moving the map off the +160 metadata gap frees seq_slot to
634        // reclaim +128..+256, lifting the concurrent-LoRA decode cap from 8 to 32.
635        let moe_row_adapter_buf = gpu.alloc(max_batch_size.max(1) * 4)?;
636
637        // Secondary stream + event for pipelining checkpoint D2D with MTP propose.
638        let secondary_stream = gpu.create_stream()?;
639        let secondary_event = gpu.create_event()?;
640        // Event ordering SSM-snapshot saves (default stream) before a warm
641        // Marconi restore (prefill stream). See `snapshot_event` doc in types.rs.
642        let snapshot_event = gpu.create_event()?;
643
644        // EP/TP: register the all-reduce target buffers with NCCL (caches the
645        // IB/RoCE memory registration, enabling zero-copy user-buffer
646        // collectives) and provide the bf16_add kernel for the 2-rank
647        // send/recv fast path.
648        //   - moe_output: EP MoE reduce + ALL GDN HeadParallel SSM out_proj
649        //     reduces (decode `ssm_forward`, batched decode, multi-seq
650        //     batched, prefill, prefill phase-3 all write out_proj into
651        //     `buffers.moe_output()`).
652        //   - norm_output: attention o_proj decode output
653        //     (`attention_forward_oproj` writes o_out = `buffers.norm_output()`),
654        //     reduced per attention layer under TP.
655        // 🔴 D1. Levers that decide the COLLECTIVE SCHEDULE are read per-rank from the
656        // environment, so a rank skew is a hang or a wrong-extent reduce rather than a perf
657        // difference. Agree on them before the first token. See `crate::rank_agree`.
658        if let Some(ref comm) = comm {
659            crate::rank_agree::assert_ranks_agree(
660                &*gpu,
661                comm.as_ref(),
662                &[
663                    // Splits a prefill chunk into sub-chunks, and every sub-chunk issues its own
664                    // `reduce_partial` at the attention site and the MLP site.
665                    (
666                        "AVAROK_GLM_PREFILL_ROWS",
667                        crate::layers::glm5next_layer::prefill_rows() as u64,
668                    ),
669                    // Perf-only (the MLP reduces once per site whichever arm runs), but a skew
670                    // here is still a confusing asymmetry and the check is free.
671                    (
672                        "AVAROK_GLM_MOE_ROW_BATCH_MAX",
673                        crate::layers::glm5next_mlp::forward::row_batch_max() as u64,
674                    ),
675                    // Documented as "both ranks must agree" in `model/types.rs` since it was
676                    // introduced, and never checked.
677                    (
678                        "AVAROK_EP_PROTOCOL(v2)",
679                        u64::from(matches!(
680                            std::env::var("AVAROK_EP_PROTOCOL").as_deref(),
681                            Ok("v2")
682                        )),
683                    ),
684                ],
685            )?;
686        }
687
688        if let Some(ref comm) = comm
689            && comm.world_size() == 2
690        {
691            let moe_ptr = buffers.moe_output().0;
692            let moe_bytes = buffers.sizes().moe_output;
693            match comm.register_buffer(moe_ptr, moe_bytes) {
694                Ok(_) => tracing::info!("Registered moe_output ({moe_bytes} B) with NCCL"),
695                Err(e) => tracing::warn!("ncclCommRegister moe_output failed (non-fatal): {e}"),
696            }
697            let norm_ptr = buffers.norm_output().0;
698            let norm_bytes = buffers.sizes().norm_output;
699            match comm.register_buffer(norm_ptr, norm_bytes) {
700                Ok(_) => tracing::info!("Registered norm_output ({norm_bytes} B) with NCCL"),
701                Err(e) => tracing::warn!("ncclCommRegister norm_output failed (non-fatal): {e}"),
702            }
703            //   - logits: the vocab-parallel BF16 LM head's all-reduce target
704            //     (`impl_a3::lm_head`). Unregistered it is the only per-step collective
705            //     whose SEND pointer NCCL has never seen, which costs an ibv_reg_mr on
706            //     the critical path of every token.
707            let logits_ptr = buffers.logits().0;
708            let logits_bytes = buffers.sizes().logits;
709            match comm.register_buffer(logits_ptr, logits_bytes) {
710                Ok(_) => tracing::info!("Registered logits ({logits_bytes} B) with NCCL"),
711                Err(e) => tracing::warn!("ncclCommRegister logits failed (non-fatal): {e}"),
712            }
713            match gpu.kernel("bf16_add", "bf16_add_inplace") {
714                Ok(k) => comm.set_add_kernel(k.0),
715                Err(e) => {
716                    tracing::warn!("bf16_add_inplace kernel not found (send/recv disabled): {e}")
717                }
718            }
719        }
720
721        // Allocate pinned host staging buffer for batched metadata H2D.
722        let pinned_bytes = buffers.sizes().scratch.max(64 * 1024);
723        let pinned_ptr = gpu.alloc_host_pinned(pinned_bytes)?;
724        tracing::info!("Pinned metadata staging: {} KB", pinned_bytes / 1024);
725        let max_batch_tokens = buffers.max_batch_tokens();
726        let pinned_staging = std::cell::UnsafeCell::new(PinnedMetaStaging {
727            ptr: pinned_ptr,
728            bytes: pinned_bytes,
729            positions: Vec::with_capacity(max_batch_tokens),
730            positions_h: Vec::with_capacity(max_batch_tokens),
731            positions_w: Vec::with_capacity(max_batch_tokens),
732            slots: Vec::with_capacity(max_batch_tokens),
733        });
734
735        // SSM state normalization kernel + pointer buffer (for chunked prefill).
736        let ssm_norm_k = gpu
737            .kernel("ssm_state_norm", "ssm_state_clamp_norm_fused")
738            .unwrap_or(KernelHandle(0));
739        let ssm_norm_f16_k = gpu
740            .kernel("ssm_state_norm", "ssm_state_clamp_norm_fused_f16")
741            .unwrap_or(KernelHandle(0));
742        let ssm_h_f32_to_f16_k =
743            crate::layers::try_kernel(gpu.as_ref(), "ssm_h_dtype", "ssm_h_state_f32_to_f16");
744        let ssm_h_f16_to_f32_k =
745            crate::layers::try_kernel(gpu.as_ref(), "ssm_h_dtype", "ssm_h_state_f16_to_f32");
746
747        // Logit softcapping (Gemma-4: cap=30.0). Only load if model uses it.
748        let logit_softcap_kernel = if config.final_logit_softcapping > 0.0 {
749            gpu.kernel("logit_softcap", "logit_softcap_bf16")
750                .unwrap_or_else(|e| {
751                    tracing::warn!("logit_softcap kernel not found: {e}");
752                    KernelHandle(0)
753                })
754        } else {
755            KernelHandle(0)
756        };
757        // FP32 softcap variant — only loaded when both softcap and FP32
758        // residual are active (i.e. Gemma-4 dense). Other models keep the
759        // BF16 softcap (or no softcap at all).
760        // The FP32 logit softcap variant required an FP32 residual stream,
761        // which no longer exists, so the BF16 softcap path is always taken.
762        let logit_softcap_fp32_kernel = KernelHandle(0);
763        // FP32 logits gate. The LM head produces FP32 (rather than BF16)
764        // logits when the residual stream is FP32 AND the LM head is a
765        // dense BF16 weight (no NVFP4 quant). NVFP4 LM heads keep their
766        // existing path because that quantization is a much larger
767        // precision floor than the BF16 store; FP32 wouldn't help there.
768        // Today this only affects Gemma-4 dense (model_type=="gemma4",
769        // num_experts==0, tied BF16 embed→lm_head).
770        // Gemma-4-31B FP32 lm_head experiment. Disabled by default —
771        // session 2026-05-01 verified the BF16 lm_head store is NOT the
772        // source of Gemma-4's haiku argmax flip: FP32 view of step-1
773        // logits keeps top1=` a` (21.85), top2=` waves` (21.706) — same
774        // 0.14-margin tiebreak as BF16. The drift is upstream in attention
775        // or MLP, not in the lm_head precision boundary. Code paths kept
776        // wired so a future bisection (Phase 2 of the plan) can re-enable
777        // via `AVAROK_GEMMA4_FP32_LMHEAD=1`. Keep `use_fp32_logits=false`
778        // by default so the rest of the model behaves identically to the
779        // pre-fix BF16 path on every model family.
780        // FP32 lm_head + softcap. Default OFF — empirically the gain on
781        // Gemma-4-31B is marginal (Creative occasionally cleaner; fib still
782        // fails the same broken-indentation pattern) but the cost is huge:
783        // FP32 forces host-side sampling (vocab=262144 × 4 bytes per
784        // decode step → ~1 MB D2H per token) which crushes decode TPS
785        // from ~35 tok/s to ~6 tok/s on Gemma-4-31B. Not worth it without
786        // a GPU-side FP32 argmax kernel. `AVAROK_GEMMA4_FP32_LMHEAD=1`
787        // re-enables for bisection / future work.
788        //
789        // The earlier "FP32 doesn't fix haiku" comment in this file was
790        // arrived at via incomplete bisection (the scheduler readback
791        // always assumed BF16 — see commit 16b2f3a's commit body). The
792        // 2026-05-01 evening run with the dispatch wired confirmed the
793        // bisection's *qualitative* conclusion: FP32 lm_head + softcap
794        // doesn't materially fix Gemma-4's structural NVFP4 attention
795        // drift on greedy code generation. Fix is upstream of lm_head.
796        // FP32 logits (AVAROK_GEMMA4_FP32_LMHEAD) required an FP32 residual
797        // stream as a precondition. With the residual stream now always BF16,
798        // the FP32 logits path can never activate, so it is permanently off.
799        let use_fp32_logits = false;
800        // Dedicated FP32 logits scratch — only the single-token decode path
801        // uses it. Prefill and batched-decode lm_head still write BF16 to the
802        // shared `buffers.logits()`. Sized for one row of `vocab_size` FP32.
803        let logits_fp32_buf = if use_fp32_logits {
804            let bytes = config.vocab_size * 4;
805            let p = gpu.alloc(bytes)?;
806            tracing::info!(
807                "FP32 LM head + softcap active (model_type={}, vocab={}). \
808                 Decode logits scratch: {} bytes.",
809                config.model_type,
810                config.vocab_size,
811                bytes,
812            );
813            p
814        } else {
815            DevicePtr::NULL
816        };
817
818        // Embedding scale (Gemma-4: sqrt(hidden_size)). Only load if model uses it.
819        let embed_scale_kernel = if config.embed_scale > 0.0 {
820            gpu.kernel("embed_scale", "bf16_scale_inplace")
821                .unwrap_or_else(|e| {
822                    tracing::warn!("embed_scale kernel not found: {e}");
823                    KernelHandle(0)
824                })
825        } else {
826            KernelHandle(0)
827        };
828        if config.embed_scale > 0.0 {
829            tracing::info!(
830                "Embedding scale: {:.4} (sqrt({}))",
831                config.embed_scale,
832                config.hidden_size
833            );
834        }
835        let ssm_norm_ptrs = if ssm_pool.num_ssm_layers > 0 {
836            gpu.alloc(ssm_pool.num_ssm_layers * 8)
837                .unwrap_or(DevicePtr::NULL)
838        } else {
839            DevicePtr::NULL
840        };
841
842        // GDN prefill buffers: sized for max_batch_tokens (the prefill chunk size),
843        // NOT max_seq_len. For prompts longer than this, prefill_twophase falls back
844        // to standard chunked prefill which carries h_state/conv_state between chunks.
845        // The GDN recurrence is sequential anyway, so chunking is mathematically identical.
846        let (gdn_qkv, gdn_gate_beta, gdn_out, gdn_z, gdn_buf_len) =
847            super::impl_a1_init::build_gdn_prefill_buffers(
848                &config,
849                max_batch_tokens,
850                max_seq_len,
851                gpu.as_ref(),
852            )?;
853
854        // FP8 calibration only runs when the cache is actually FP8 — the
855        // observe() call in decode.rs sits inside the FP8 cache branch. For
856        // BF16 or NVFP4 caches the MODEL.toml fp8_kv_calibration_tokens
857        // value is dead code and must not suppress CUDA graphs.
858        let has_fp8_calibration = config.fp8_kv_calibration_tokens > 0
859            && kv_cache.dtype() == spark_runtime::kv_cache::KvCacheDtype::Fp8;
860        // Feature-2 overlay kernels: resolve before `gpu` is moved into Self.
861        let overlay_kernels = crate::layers::ops::token_overlay::OverlayKernels::new(gpu.as_ref());
862        Ok(Self {
863            // Installed by the factory after construction: the layers read
864            // from the store during `new`, so it cannot be moved in here.
865            weight_store: None,
866            config,
867            dispatch: crate::layers::ops::GemmDispatch::from_env(),
868            derived: crate::layers::ops::DerivedWeights::new(),
869            levers,
870            stats: ops::ModelStats::new(),
871            #[cfg(feature = "cuda")]
872            innerq: gpu.kernel_registry().and_then(|reg| {
873                let driver = crate::layers::qwen3_attention::InnerQDriver::from_env(reg)?;
874                match driver.start() {
875                    Ok(()) => Some(driver),
876                    Err(e) => {
877                        tracing::warn!("InnerQ calibration disabled: start() failed: {e:#}");
878                        None
879                    }
880                }
881            }),
882            embed_tokens,
883            ngram_embed: None,
884            final_norm,
885            lm_head_weight,
886            lm_head_nvfp4,
887            lm_head_nvfp4_t,
888            lm_head_fp8,
889            lm_head_q6k: None,
890            // ★ Before `layers` is moved: the veto is a fold over the layers
891            // and must be computed while they are still nameable here.
892            decode_graph_veto: layers.iter().any(|l| l.decode_graph_unsupported()),
893            layers,
894            buffers,
895            lora: None,
896            lora_rotatable: false,
897            kv_cache: Mutex::new(kv_cache),
898            gpu,
899            rms_norm_kernel,
900            dense_gemv_kernel,
901            dense_gemv_fp32out_kernel,
902            w4a16_gemv_kernel,
903            w4a16_gemv_logits_kernel,
904            w4a16_gemm_t_kernel,
905            w4a16_gemm_t_bf16_kernel,
906            w4a16_gemm_kernel,
907            w4a16_gemv_batch2_kernel,
908            w4a16_batchm,
909            w4a16_gemv_batch16_kernel,
910            dense_gemv_fp8w_kernel,
911            dense_gemv_fp8w_batch2_kernel,
912            dense_gemm_kernel,
913            dense_gemv_batchm_kernel,
914            lm_head_m16_tc_kernel,
915            lm_head_m16_tc_n64_kernel,
916            argmax_kernel,
917            argmax_batch_kernel,
918            argmax_logits_kernel,
919            batched_embed_kernel,
920            fill_slots_kernel,
921            decode_graph: Mutex::new(std::collections::HashMap::new()),
922            batch_decode_graphs: Mutex::new((HashMap::new(), 0)),
923            // Suppress graphs during FP8 calibration only. MLA used to be
924            // suppressed because an internal sync was placed inside the graph
925            // capture region — that sync is now conditional on eager mode
926            // (see line ~3881), so graphs work for MLA too. The zero_all call
927            // at line ~3751 runs in Phase 1 BEFORE begin_capture, so it is
928            // naturally outside the captured region.
929            suppress_graphs: std::sync::atomic::AtomicBool::new(
930                has_fp8_calibration
931                    || std::env::var("AVAROK_DIAG_GEMMA4").is_ok_and(|v| v == "1" || v == "true")
932                    // PCND diagnostic: force eager decode (no CUDA-graph capture)
933                    // so AVAROK_DEBUG_SYNC_KERNELS can synchronize per launch and
934                    // surface async faults at the culprit kernel. Default-off.
935                    || std::env::var("AVAROK_DEBUG_NO_GRAPH").as_deref() == Ok("1"),
936            ),
937            ssm_pool,
938            ssm_snapshots,
939            ssm_tier_store,
940            max_blocks_per_seq,
941            dummy_kv_block,
942            profile,
943            profile_first_pending: std::sync::atomic::AtomicBool::new(profile_first),
944            proposer,
945            mtp_hidden_save,
946            verify_hidden_stash,
947            mtp_catchup_ring,
948            mtp_catchup_meta: parking_lot::Mutex::new((0, 0)),
949            mtp_prefill_hidden,
950            // SSOT for the capture bounds check: the ROW COUNT actually
951            // allocated (both ceilings applied), never `max_seq_len`. The
952            // capture guard in `drafter_prefill.rs` compares against this.
953            mtp_prefill_capacity: if mtp_prefill_hidden.is_null() {
954                0
955            } else {
956                capture_rows
957            },
958            mtp_prefill_capture_len: std::sync::atomic::AtomicUsize::new(0),
959            mtp_prefill_capture_gen: std::sync::atomic::AtomicU64::new(0),
960            mtp_store_gen_seq: std::sync::atomic::AtomicU64::new(0),
961            mtp_carry: parking_lot::Mutex::new(None),
962            mtp_store_range: parking_lot::Mutex::new(super::mtp_carry::StoreRange::EMPTY),
963            dflash_hidden_save,
964            verify_ptok_meta: std::sync::OnceLock::new(),
965            dflash_hidden_save_rows,
966            dflash_kgamma,
967            dflash_capture_layers,
968            verify2_graph: Mutex::new(std::collections::HashMap::new()),
969            verify3_graph: Mutex::new(std::collections::HashMap::new()),
970            verify4_graph: Mutex::new(std::collections::HashMap::new()),
971            verify_batched_graphs: Mutex::new((std::collections::HashMap::new(), 0)),
972            verify_wy_tables,
973            gdn_woa_na_tab,
974            gdn_woa_folded_slots: parking_lot::Mutex::new(Vec::new()),
975            gdn_woa_eligible: std::sync::atomic::AtomicBool::new(false),
976            gdn_woa_bound: parking_lot::Mutex::new((DevicePtr::NULL, DevicePtr::NULL, 0)),
977            // Nothing staged yet: the buffer was memset to zero above, and no
978            // key describes zero, so the first verify step always uploads.
979            verify_wy_cache: Mutex::new(None),
980            verify_kgamma_graph: Mutex::new(std::collections::HashMap::new()),
981            fused_graph: Mutex::new(std::collections::HashMap::new()),
982            prefix_cache,
983            secondary_stream,
984            secondary_event,
985            snapshot_event,
986            comm,
987            ep_cmd_buf,
988            ep_protocol_v2: matches!(std::env::var("AVAROK_EP_PROTOCOL").as_deref(), Ok("v2")),
989            self_speculative,
990            last_mtp_hidden_idx: std::sync::atomic::AtomicUsize::new(0),
991            vision_encoder,
992            vision_embed_patches: Mutex::new(0),
993            vision_image_grids: Mutex::new(Vec::new()),
994            vision_row_base: Mutex::new(0),
995            vision_grid_base: Mutex::new(0),
996            vision_owned_images: Mutex::new(0),
997            pinned_staging,
998            ssm_checkpoint_interval,
999            ssm_state_norm_kernel: ssm_norm_k,
1000            ssm_state_norm_f16_kernel: ssm_norm_f16_k,
1001            ssm_h_f32_to_f16_kernel: ssm_h_f32_to_f16_k,
1002            ssm_h_f16_to_f32_kernel: ssm_h_f16_to_f32_k,
1003            ssm_h_f16_scratch: std::sync::OnceLock::new(),
1004            ssm_norm_ptrs_buf: ssm_norm_ptrs,
1005            moe_row_adapter_buf,
1006            gdn_buf_qkv: gdn_qkv,
1007            gdn_buf_gate_beta: gdn_gate_beta,
1008            gdn_buf_out: gdn_out,
1009            gdn_buf_z: gdn_z,
1010            gdn_buf_max_len: gdn_buf_len,
1011            logit_softcap_kernel,
1012            logit_softcap_fp32_kernel,
1013            use_fp32_logits,
1014            logits_fp32_buf,
1015            embed_scale_kernel,
1016            overlays: None,
1017            overlay_kernels,
1018            overlay_route_slot: std::sync::atomic::AtomicI32::new(-1),
1019            decode_moe_route: std::sync::atomic::AtomicI32::new(1), // Fold (safe default)
1020        })
1021    }
1022}