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