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