spark_model/model/impl_a3.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/// Presence kill switch for the wide (9..=VERIFY_ROW_CAP row) batched LM head arm:
31/// `ATLAS_NO_LMHEAD_BATCHED_WIDE` restores the M64-tile `w4a16_gemm` fallback.
32/// Presence, not value — `=0` is NOT "off" (see `atlas_env_presence_check_trap`).
33fn lmhead_batched_wide_enabled() -> bool {
34 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
35 *ON.get_or_init(|| std::env::var("ATLAS_NO_LMHEAD_BATCHED_WIDE").is_err())
36}
37
38impl TransformerModel {
39 /// Scale in-place embeddings by config.embed_scale. The residual stream
40 /// is always BF16, so this dispatches `embed_scale::bf16_scale_inplace`.
41 pub(super) fn scale_embeddings(
42 &self,
43 data: DevicePtr,
44 num_tokens: usize,
45 stream: u64,
46 ) -> Result<()> {
47 self.scale_embeddings_bf16(data, num_tokens, stream)
48 }
49
50 pub(super) fn scale_embeddings_bf16(
51 &self,
52 data: DevicePtr,
53 num_tokens: usize,
54 stream: u64,
55 ) -> Result<()> {
56 if self.embed_scale_kernel.0 == 0 {
57 return Ok(());
58 }
59 use spark_runtime::kernel_args::KernelLaunch;
60 let n = (num_tokens * self.config.hidden_size) as u32;
61 KernelLaunch::new(self.gpu.as_ref(), self.embed_scale_kernel)
62 .grid([n.div_ceil(256), 1, 1])
63 .block([256, 1, 1])
64 .arg_ptr(data)
65 .arg_u32(n)
66 .arg_f32(self.config.embed_scale)
67 .launch(stream)
68 }
69
70 /// Wide batched LM head, `num_tokens` in 9..=`VERIFY_ROW_CAP` (96) — exactly
71 /// the batched-verify row regime (`can_batch_verify` bounds `Σks` at
72 /// `VERIFY_ROW_CAP`; the 32:2 depth-at-width shape's n=32 × k=3 rows hits
73 /// 96 dead on).
74 /// Below 9 the existing GEMV ladder (batch2/4/8) already owns the dispatch.
75 /// When this arm was capped at 32 (pre-2026-07-30), the R=64 verify step
76 /// fell through to the base `w4a16_gemm` below at 23.9 ms/step (nsys,
77 /// spec32n) vs 3.65 ms for the same 636 MB via this tile GEMM at R=32.
78 ///
79 /// Why it matters: every row count in that range fell through to the M64-tile
80 /// `w4a16_gemm`, whose own comment upstream documents the cost — nsys measured
81 /// **19.9 ms per verify step** on the [248320, 5120] NVFP4 head, ~33 GB/s
82 /// effective, because 94%+ of the M-tile is padding. The transposed-twin tile
83 /// GEMM streams the same 636 MB once at near-roofline. This is the identical
84 /// ladder `decode_a2` already runs at `padded_n >= 5`, so the verify head now
85 /// uses the SAME kernel as the non-speculative decode head it is standing in
86 /// for (it previously did not — a silent numerics divergence between the spec
87 /// and non-spec paths at the same batch width).
88 ///
89 /// Returns `false` when no wide kernel resolves (no NVFP4 head, no twin, no
90 /// batch16 handle), in which case the caller keeps today's `w4a16_gemm`.
91 fn lm_head_batched_wide(
92 &self,
93 hidden: DevicePtr,
94 num_tokens: u32,
95 logits: DevicePtr,
96 stream: u64,
97 ) -> Result<bool> {
98 let h = self.config.hidden_size as u32;
99 let v = self.config.vocab_size as u32;
100 let Some(ref nvfp4) = self.lm_head_nvfp4 else {
101 return Ok(false);
102 };
103 if let Some((ref nvfp4_t, ldb)) = self.lm_head_nvfp4_t {
104 // LOSSLESS path (ATLAS_LMHEAD_LOSSLESS): BF16 MMA, no activation
105 // downcast. Mirrors decode_a2 so the two heads never disagree.
106 if self.w4a16_gemm_t_bf16_kernel.0 != 0 {
107 ops::w4a16_gemm_n128_m128_bf16_ldb(
108 self.gpu.as_ref(),
109 self.w4a16_gemm_t_bf16_kernel,
110 hidden,
111 nvfp4_t,
112 logits,
113 num_tokens,
114 v,
115 h,
116 ldb,
117 stream,
118 )?;
119 return Ok(true);
120 }
121 if self.w4a16_gemm_t_kernel.0 != 0 {
122 ops::w4a16_gemm_n128_ldb(
123 self.gpu.as_ref(),
124 self.w4a16_gemm_t_kernel,
125 hidden,
126 nvfp4_t,
127 logits,
128 num_tokens,
129 v,
130 h,
131 ldb,
132 stream,
133 )?;
134 return Ok(true);
135 }
136 }
137 // No twin (ATLAS_NO_LMHEAD_TGEMM): the M<=16 weight-streaming GEMV still
138 // beats the M64 tile. M=17..32 has no single-read form, so it keeps the
139 // GEMM rather than paying two full weight passes.
140 if num_tokens <= 16 && self.w4a16_gemv_batch16_kernel.0 != 0 {
141 ops::w4a16_gemv_batchm(
142 self.gpu.as_ref(),
143 self.w4a16_gemv_batch16_kernel,
144 hidden,
145 nvfp4,
146 logits,
147 num_tokens,
148 v,
149 h,
150 stream,
151 )?;
152 return Ok(true);
153 }
154 Ok(false)
155 }
156
157 /// LM head for K tokens: hidden[K, H] → logits[K, V].
158 pub(super) fn lm_head_batched(
159 &self,
160 hidden: DevicePtr,
161 num_tokens: u32,
162 logits_dst: DevicePtr,
163 stream: u64,
164 ) -> Result<DevicePtr> {
165 let h = self.config.hidden_size as u32;
166 let v = self.config.vocab_size as u32;
167 // Caller picks the destination so co-dispatched prefill streams can each
168 // write their own logits row (was a single shared buffer = cross-stream
169 // aliasing: all streams' first token collapsed to one). Verify/decode
170 // callers pass `self.buffers.logits()` (base) — unchanged behaviour.
171 let logits = logits_dst;
172 if let Some(ref fp8) = self.lm_head_fp8 {
173 // FP8 E4M3 LM head. The dual-GEMV (batch=2) reads the FP8 weight
174 // once for both K=2 verify tokens — bit-identical to two M=1 GEMVs
175 // but halves the full-vocab weight bandwidth. Falls back to the
176 // per-token loop for K!=2 or when the kernel is absent.
177 let bf16 = 2usize;
178 if num_tokens == 2 && self.dense_gemv_fp8w_batch2_kernel.0 != 0 {
179 ops::dense_gemv_fp8w_batch2(
180 self.gpu.as_ref(),
181 self.dense_gemv_fp8w_batch2_kernel,
182 hidden,
183 fp8,
184 logits,
185 v,
186 h,
187 stream,
188 )?;
189 } else {
190 for i in 0..num_tokens as usize {
191 ops::dense_gemv_fp8w(
192 self.gpu.as_ref(),
193 self.dense_gemv_fp8w_kernel,
194 hidden.offset(i * h as usize * bf16),
195 fp8,
196 logits.offset(i * v as usize * bf16),
197 v,
198 h,
199 stream,
200 )?;
201 }
202 }
203 } else if self.lm_head_nvfp4.is_none()
204 && (2..=ops::DENSE_GEMV_BATCHM_DECODE_MAX_M).contains(&num_tokens)
205 && self.dense_gemv_batchm_kernel.0 != 0
206 {
207 // BF16 head, 2..8 verify rows: ONE sweep over `[vocab, hidden]` for every row.
208 //
209 // 🔴 This arm exists because the NVFP4 tiers below do not cover a BF16 head, so a
210 // BF16-head model fell through to "2x dense_gemv" — and on GLM-5.3 that measured
211 // **+5.57 ms per extra verify row** (nsys, 2026-08-29, differential between 365
212 // serial and 147 verify steps), the largest single item after the routed experts.
213 // The vocab is 154,880 x 4,096 BF16 = 1.27 GB, so re-reading it per row is the
214 // whole cost.
215 //
216 // Bit-identical to the per-row GEMVs it replaces: `dense_gemv_bf16_batchm`
217 // reproduces each row's K-iteration order and reduction tree.
218 let (w, n, dst) = match self.lmhead_vocab_shard(v) {
219 // VOCAB-PARALLEL, same construction as the single-token arm: each rank
220 // computes a contiguous row range of an otherwise REPLICATED head, the rest is
221 // zeroed, and the pieces are summed. Byte-identical rather than merely close —
222 // logit `n` is one full dot product over K = hidden by the same kernel in the
223 // same order, only WHICH rank runs it changes, and BF16 `x + 0` is exact.
224 //
225 // 🪤 Rests on `hidden` being bit-identical on every rank here. It is: the last
226 // thing the layer stack does is an all-reduce. The byte-identity gate is what
227 // actually checks that assumption.
228 Some((begin, len)) => {
229 self.gpu.memset_async(
230 logits,
231 0,
232 num_tokens as usize * v as usize * 2,
233 stream,
234 )?;
235 (
236 crate::weight_map::DenseWeight {
237 weight: self.lm_head_weight.weight.offset(begin * h as usize * 2),
238 },
239 len as u32,
240 logits.offset(begin * 2),
241 )
242 }
243 None => (
244 crate::weight_map::DenseWeight {
245 weight: self.lm_head_weight.weight,
246 },
247 v,
248 logits,
249 ),
250 };
251 ops::dense_gemv_batchm(
252 self.gpu.as_ref(),
253 self.dense_gemv_batchm_kernel,
254 hidden,
255 &w,
256 dst,
257 num_tokens,
258 n,
259 h,
260 // Rows of `logits` are a full vocab apart even when this rank writes a slice.
261 v,
262 stream,
263 )?;
264 if n != v
265 && let Some(comm) = self.comm_ref()
266 {
267 comm.all_reduce_async(logits.0, num_tokens as usize * v as usize * 2, stream)?;
268 }
269 } else if num_tokens == 2 {
270 // Double-GEMV: reads weights once, computes 2 outputs.
271 // GEMM M=2 with 64×64 tiles wastes 97% of M-dimension → ~3× slower.
272 if let Some(ref nvfp4) = self.lm_head_nvfp4 {
273 ops::w4a16_gemv_batch2(
274 self.gpu.as_ref(),
275 self.w4a16_gemv_batch2_kernel,
276 hidden,
277 nvfp4,
278 logits,
279 v,
280 h,
281 stream,
282 )?;
283 } else {
284 // Dense fallback: 2× GEMV. Stays BF16 even when
285 // use_fp32_logits is on — the FP32 path is decode-only
286 // (single-token `lm_head`); batched-decode/prefill keeps
287 // BF16 because the bug it fixes only manifests at decode
288 // step 1 (first-token argmax tiebreak).
289 ops::dense_gemv(
290 self.gpu.as_ref(),
291 self.dense_gemv_kernel,
292 hidden,
293 &self.lm_head_weight,
294 logits,
295 v,
296 h,
297 stream,
298 )?;
299 ops::dense_gemv(
300 self.gpu.as_ref(),
301 self.dense_gemv_kernel,
302 hidden.offset(h as usize * 2),
303 &self.lm_head_weight,
304 logits.offset(v as usize * 2),
305 v,
306 h,
307 stream,
308 )?;
309 }
310 } else if (3..=8).contains(&num_tokens)
311 && self.w4a16_batchm.kernel(num_tokens).0 != 0
312 && let Some(ref nvfp4) = self.lm_head_nvfp4
313 {
314 // K=3..8 verify lm_head: one weight read for all rows via the
315 // narrowest batched-GEMV tier covering the row count. nsys
316 // (2026-07-18, drafts=3 serve): the base M64-tile `w4a16_gemm`
317 // below cost 19.3 ms/verify-step on the [248320, 5120] NVFP4
318 // lm_head at M=4 — 94% of the M-tile is padding, ~33 GB/s
319 // effective. The batch GEMV streams the same 636 MB once at
320 // near-peak (~2.5 ms), the single largest slice of the K=4
321 // verify-vs-K=2 cost gap; the M=5..8 tiers extend that to the
322 // chain-verify rows (batchm_bench).
323 ops::w4a16_gemv_batchm(
324 self.gpu.as_ref(),
325 self.w4a16_batchm.kernel(num_tokens),
326 hidden,
327 nvfp4,
328 logits,
329 num_tokens,
330 v,
331 h,
332 stream,
333 )?;
334 } else if (9..=super::trait_impl::verify_e2::VERIFY_ROW_CAP as u32).contains(&num_tokens)
335 && lmhead_batched_wide_enabled()
336 && self.lm_head_batched_wide(hidden, num_tokens, logits, stream)?
337 {
338 // Handled by the wide arm; nothing launched when it returns false.
339 } else if let Some(ref nvfp4) = self.lm_head_nvfp4 {
340 ops::w4a16_gemm(
341 self.gpu.as_ref(),
342 self.w4a16_gemm_kernel,
343 hidden,
344 nvfp4,
345 logits,
346 num_tokens,
347 v,
348 h,
349 stream,
350 )?;
351 } else {
352 ops::dense_gemm(
353 self.gpu.as_ref(),
354 self.dense_gemm_kernel,
355 hidden,
356 &self.lm_head_weight,
357 logits,
358 num_tokens,
359 v,
360 h,
361 stream,
362 )?;
363 }
364 // Feature-2: overlay overridden logit columns AFTER the base projection,
365 // BEFORE softcap. Uniform-active route (seq_slot NULL); BF16 logits.
366 // No-op when no overlay is installed.
367 self.apply_lmhead_overlay(hidden, DevicePtr(0), logits, num_tokens, false, stream)?;
368 // Apply logit softcapping: logits = cap * tanh(logits / cap)
369 if self.logit_softcap_kernel.0 != 0 {
370 let cap = self.config.final_logit_softcapping;
371 let total = num_tokens * v;
372 self.apply_logit_softcap(logits, total, cap, stream)?;
373 }
374 Ok(logits)
375 }
376
377 /// Row range of the BF16 LM head this rank should compute, or `None` to keep the
378 /// replicated whole-vocab projection.
379 ///
380 /// `None` unless there is a real multi-rank communicator and the vocab divides evenly.
381 /// Kill switch: `ATLAS_NO_LMHEAD_VOCAB_TP=1`. Only reachable from the plain BF16 dense
382 /// head — the FP8 / NVFP4 / FP32-logits heads keep their existing path untouched.
383 fn lmhead_vocab_shard(&self, v: u32) -> Option<(usize, usize)> {
384 static OFF: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
385 if *OFF.get_or_init(|| std::env::var("ATLAS_NO_LMHEAD_VOCAB_TP").as_deref() == Ok("1")) {
386 return None;
387 }
388 let comm = self.comm_ref()?;
389 let ws = comm.world_size();
390 if ws < 2 || !(v as usize).is_multiple_of(ws) {
391 return None;
392 }
393 let len = v as usize / ws;
394 Some((comm.rank() * len, len))
395 }
396
397 pub(super) fn lm_head(&self, hidden: DevicePtr, stream: u64) -> Result<DevicePtr> {
398 let h = self.config.hidden_size as u32;
399 let v = self.config.vocab_size as u32;
400 // Pick the output buffer: FP32 scratch when use_fp32_logits is on,
401 // shared BF16 buffer otherwise. The sampler must use the matching
402 // dtype — see `decode_logits_dtype()`.
403 let (logits, fp32) = if self.use_fp32_logits {
404 (self.logits_fp32_buf, true)
405 } else {
406 (self.buffers.logits(), false)
407 };
408 if let Some(ref fp8) = self.lm_head_fp8 {
409 // FP8 E4M3 LM head (`--lm-head-dtype fp8`). `w8a16_gemv` has no
410 // FP32-output variant — it writes to whichever buffer is passed.
411 // `use_fp32_logits` is false in production, so `logits` is the
412 // shared BF16 buffer; the FP32-logits path is unused here.
413 ops::dense_gemv_fp8w(
414 self.gpu.as_ref(),
415 self.dense_gemv_fp8w_kernel,
416 hidden,
417 fp8,
418 logits,
419 v,
420 h,
421 stream,
422 )?;
423 } else if let Some(ref nvfp4) = self.lm_head_nvfp4 {
424 // Pick FP32-output variant when the FP32 logits buffer is the
425 // destination. Same packed-NVFP4 weights, same activation, but the
426 // accumulator is NOT downcast to BF16 — closes the 0.125-logit
427 // BF16-rounding tiebreak flip that triggers Gemma-4-31B's
428 // creative-collapse stop-word loop.
429 let kernel = if fp32 {
430 self.w4a16_gemv_logits_kernel
431 } else {
432 self.w4a16_gemv_kernel
433 };
434 ops::w4a16_gemv(
435 self.gpu.as_ref(),
436 kernel,
437 hidden,
438 nvfp4,
439 logits,
440 v,
441 h,
442 stream,
443 )?;
444 } else if fp32 {
445 // FP32-output dense GEMV: same precision-preservation reason as
446 // the NVFP4 variant above. Used when Gemma keeps the LM head
447 // as BF16 (skip_lm_head_quantization=true).
448 ops::dense_gemv(
449 self.gpu.as_ref(),
450 self.dense_gemv_fp32out_kernel,
451 hidden,
452 &self.lm_head_weight,
453 logits,
454 v,
455 h,
456 stream,
457 )?;
458 } else if let Some((begin, len)) = self.lmhead_vocab_shard(v) {
459 // VOCAB-PARALLEL BF16 head. The BF16 `lm_head` is REPLICATED on every rank —
460 // no loader shards it — so at TP=2 both ranks stream the whole vocab weight
461 // for the same logits. On GLM-5.3 that is 1.27 GB and **5.49 ms of an 79 ms
462 // decode step** (nsys, 2026-08-28), the single largest kernel after the KDA
463 // projections.
464 //
465 // Each rank computes a contiguous row range instead. This is BYTE-IDENTICAL
466 // by construction, not by tolerance: logit `n` is one full dot product over
467 // K=hidden run by the same kernel with the same reduction order — only WHICH
468 // rank runs it changes. The rest of the buffer is zeroed and the pieces are
469 // summed, and BF16 `x + 0` is exact, so the assembled vector is the same
470 // bytes the replicated head produced.
471 //
472 // 🪤 Correctness rests on `hidden` being bit-identical on every rank at this
473 // point. It is: the last thing the layer stack does is an all-reduce, which
474 // lands the same bytes everywhere. The byte-identity gate is what actually
475 // checks it — if that assumption ever breaks, the completions diverge.
476 self.gpu.memset_async(logits, 0, v as usize * 2, stream)?;
477 ops::dense_gemv(
478 self.gpu.as_ref(),
479 self.dense_gemv_kernel,
480 hidden,
481 &crate::weight_map::DenseWeight {
482 weight: self.lm_head_weight.weight.offset(begin * h as usize * 2),
483 },
484 logits.offset(begin * 2),
485 len as u32,
486 h,
487 stream,
488 )?;
489 if let Some(comm) = self.comm_ref() {
490 comm.all_reduce_async(logits.0, v as usize * 2, stream)?;
491 }
492 } else {
493 ops::dense_gemv(
494 self.gpu.as_ref(),
495 self.dense_gemv_kernel,
496 hidden,
497 &self.lm_head_weight,
498 logits,
499 v,
500 h,
501 stream,
502 )?;
503 }
504 // Feature-2: overlay overridden logit columns AFTER the base projection,
505 // BEFORE softcap. Single-token; `fp32` selects the f32-logits kernel.
506 // No-op when no overlay is installed.
507 self.apply_lmhead_overlay(hidden, DevicePtr(0), logits, 1, fp32, stream)?;
508 // Apply logit softcapping: logits = cap * tanh(logits / cap)
509 if self.logit_softcap_kernel.0 != 0 || self.logit_softcap_fp32_kernel.0 != 0 {
510 let cap = self.config.final_logit_softcapping;
511 self.apply_logit_softcap_dtype(logits, v, cap, fp32, stream)?;
512 }
513 Ok(logits)
514 }
515
516 /// Apply logit softcapping in-place: `logits[i] = cap * tanh(logits[i] / cap)`.
517 /// BF16 path. Use `apply_logit_softcap_dtype` to dispatch by buffer dtype.
518 pub(super) fn apply_logit_softcap(
519 &self,
520 logits: DevicePtr,
521 num_elements: u32,
522 cap: f32,
523 stream: u64,
524 ) -> Result<()> {
525 use spark_runtime::kernel_args::KernelLaunch;
526 let inv_cap = 1.0f32 / cap;
527 KernelLaunch::new(self.gpu.as_ref(), self.logit_softcap_kernel)
528 .grid([num_elements.div_ceil(256), 1, 1])
529 .block([256, 1, 1])
530 .arg_ptr(logits)
531 .arg_u32(num_elements)
532 .arg_f32(inv_cap)
533 .arg_f32(cap)
534 .launch(stream)
535 }
536
537 /// Dtype-aware softcap dispatcher. Picks the BF16 or FP32 kernel based on
538 /// whether the buffer holds FP32 logits. No-op when softcap is disabled
539 /// (cap == 0). Used by the single-token decode `lm_head` to keep the FP32
540 /// path symmetrical when `use_fp32_logits` is on.
541 pub(super) fn apply_logit_softcap_dtype(
542 &self,
543 logits: DevicePtr,
544 num_elements: u32,
545 cap: f32,
546 is_fp32: bool,
547 stream: u64,
548 ) -> Result<()> {
549 use spark_runtime::kernel_args::KernelLaunch;
550 let kernel = if is_fp32 {
551 self.logit_softcap_fp32_kernel
552 } else {
553 self.logit_softcap_kernel
554 };
555 if kernel.0 == 0 {
556 return Ok(());
557 }
558 let inv_cap = 1.0f32 / cap;
559 KernelLaunch::new(self.gpu.as_ref(), kernel)
560 .grid([num_elements.div_ceil(256), 1, 1])
561 .block([256, 1, 1])
562 .arg_ptr(logits)
563 .arg_u32(num_elements)
564 .arg_f32(inv_cap)
565 .arg_f32(cap)
566 .launch(stream)
567 }
568
569 /// True when single-token decode `lm_head` writes FP32 logits to
570 /// `logits_fp32_buf`. Callers that consume those logits (sampler) MUST
571 /// read with the matching dtype. Prefill / batched-decode lm_head still
572 /// produce BF16, so this only applies to the `lm_head` (single-token)
573 /// return value.
574 pub fn decode_logits_fp32(&self) -> bool {
575 self.use_fp32_logits
576 }
577
578 /// Buffer pointer the single-token decode `lm_head` last wrote to. FP32
579 /// scratch when `use_fp32_logits`, otherwise the shared BF16 logits
580 /// buffer. Callers that previously hard-coded `self.buffers.logits()`
581 /// after `self.lm_head(...)` must use this so the sampler reads the
582 /// correct buffer dtype (the BF16 buffer is stale/empty in the FP32
583 /// path because lm_head writes elsewhere). Pair with
584 /// `logits_ptr_is_fp32` / `decode_logits_fp32` for dtype-aware reads.
585 pub fn decode_logits_ptr(&self) -> DevicePtr {
586 if self.use_fp32_logits {
587 self.logits_fp32_buf
588 } else {
589 self.buffers.logits()
590 }
591 }
592}