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}