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 avarok_core::config::{LayerType, ModelConfig};
11use spark_runtime::buffers::BufferArena;
12use spark_runtime::gpu::{DevicePtr, GpuBackend, GraphHandle, KernelHandle};
13use spark_runtime::kv_cache::PagedKvCache;
14
15use super::block_mgmt::{
16    apply_evicted_blocks, ensure_blocks_through_decode, ensure_blocks_through_prefill,
17    extract_layer_refs, reuse_prefix_match_disk_ids,
18};
19use super::ssm_pool::SsmStatePool;
20use super::ssm_snapshot::SsmSnapshotPool;
21use super::types::{PinnedMetaStaging, TransformerModel};
22use crate::layer::{
23    AttnMetadataDev, ForwardContext, GdnPrefillBuffers, LayerState, SsmLayerState, TransformerLayer,
24};
25use crate::layers::ops;
26use crate::speculative::DraftProposer;
27use crate::traits::{ChunkedPrefillPageMetadata, Model, SequenceState};
28use crate::weight_map::{DenseWeight, MtpWeights, QuantizedWeight};
29
30/// Presence kill switch for the wide (9..=VERIFY_ROW_CAP row) batched LM head arm:
31/// `AVAROK_NO_LMHEAD_BATCHED_WIDE` restores the M64-tile `w4a16_gemm` fallback.
32/// Presence, not value — `=0` is NOT "off" (see `avarok_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("AVAROK_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 (AVAROK_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 (AVAROK_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 self.lm_head_q6k_run(hidden, num_tokens, logits, stream)? {
173            return Ok(logits);
174        }
175        if let Some(ref fp8) = self.lm_head_fp8 {
176            // FP8 E4M3 LM head. The dual-GEMV (batch=2) reads the FP8 weight
177            // once for both K=2 verify tokens — bit-identical to two M=1 GEMVs
178            // but halves the full-vocab weight bandwidth. Falls back to the
179            // per-token loop for K!=2 or when the kernel is absent.
180            let bf16 = 2usize;
181            if num_tokens == 2 && self.dense_gemv_fp8w_batch2_kernel.0 != 0 {
182                ops::dense_gemv_fp8w_batch2(
183                    self.gpu.as_ref(),
184                    self.dense_gemv_fp8w_batch2_kernel,
185                    hidden,
186                    fp8,
187                    logits,
188                    v,
189                    h,
190                    stream,
191                )?;
192            } else {
193                for i in 0..num_tokens as usize {
194                    ops::dense_gemv_fp8w(
195                        self.gpu.as_ref(),
196                        self.dense_gemv_fp8w_kernel,
197                        hidden.offset(i * h as usize * bf16),
198                        fp8,
199                        logits.offset(i * v as usize * bf16),
200                        v,
201                        h,
202                        stream,
203                    )?;
204                }
205            }
206        } else if self.lm_head_nvfp4.is_none()
207            && (2..=ops::DENSE_GEMV_BATCHM_DECODE_MAX_M).contains(&num_tokens)
208            && self.dense_gemv_batchm_kernel.0 != 0
209        {
210            // BF16 head, 2..8 verify rows: ONE sweep over `[vocab, hidden]` for every row.
211            //
212            // 🔴 This arm exists because the NVFP4 tiers below do not cover a BF16 head, so a
213            // BF16-head model fell through to "2x dense_gemv" — and on GLM-5.3 that measured
214            // **+5.57 ms per extra verify row** (nsys, 2026-08-29, differential between 365
215            // serial and 147 verify steps), the largest single item after the routed experts.
216            // The vocab is 154,880 x 4,096 BF16 = 1.27 GB, so re-reading it per row is the
217            // whole cost.
218            //
219            // Bit-identical to the per-row GEMVs it replaces: `dense_gemv_bf16_batchm`
220            // reproduces each row's K-iteration order and reduction tree.
221            let (w, n, dst) = match self.lmhead_vocab_shard(v) {
222                // VOCAB-PARALLEL, same construction as the single-token arm: each rank
223                // computes a contiguous row range of an otherwise REPLICATED head, the rest is
224                // zeroed, and the pieces are summed. Byte-identical rather than merely close —
225                // logit `n` is one full dot product over K = hidden by the same kernel in the
226                // same order, only WHICH rank runs it changes, and BF16 `x + 0` is exact.
227                //
228                // 🪤 Rests on `hidden` being bit-identical on every rank here. It is: the last
229                // thing the layer stack does is an all-reduce. The byte-identity gate is what
230                // actually checks that assumption.
231                Some((begin, len)) => {
232                    self.gpu.memset_async(
233                        logits,
234                        0,
235                        num_tokens as usize * v as usize * 2,
236                        stream,
237                    )?;
238                    (
239                        crate::weight_map::DenseWeight {
240                            weight: self.lm_head_weight.weight.offset(begin * h as usize * 2),
241                        },
242                        len as u32,
243                        logits.offset(begin * 2),
244                    )
245                }
246                None => (
247                    crate::weight_map::DenseWeight {
248                        weight: self.lm_head_weight.weight,
249                    },
250                    v,
251                    logits,
252                ),
253            };
254            ops::dense_gemv_batchm(
255                self.gpu.as_ref(),
256                self.dense_gemv_batchm_kernel,
257                hidden,
258                &w,
259                dst,
260                num_tokens,
261                n,
262                h,
263                // Rows of `logits` are a full vocab apart even when this rank writes a slice.
264                v,
265                stream,
266            )?;
267            if n != v
268                && let Some(comm) = self.comm_ref()
269            {
270                comm.all_reduce_async(logits.0, num_tokens as usize * v as usize * 2, stream)?;
271            }
272        } else if num_tokens == 2 {
273            // Double-GEMV: reads weights once, computes 2 outputs.
274            // GEMM M=2 with 64×64 tiles wastes 97% of M-dimension → ~3× slower.
275            if let Some(ref nvfp4) = self.lm_head_nvfp4 {
276                ops::w4a16_gemv_batch2(
277                    self.gpu.as_ref(),
278                    self.w4a16_gemv_batch2_kernel,
279                    hidden,
280                    nvfp4,
281                    logits,
282                    v,
283                    h,
284                    stream,
285                )?;
286            } else {
287                // Dense fallback: 2× GEMV. Stays BF16 even when
288                // use_fp32_logits is on — the FP32 path is decode-only
289                // (single-token `lm_head`); batched-decode/prefill keeps
290                // BF16 because the bug it fixes only manifests at decode
291                // step 1 (first-token argmax tiebreak).
292                ops::dense_gemv(
293                    self.gpu.as_ref(),
294                    self.dense_gemv_kernel,
295                    hidden,
296                    &self.lm_head_weight,
297                    logits,
298                    v,
299                    h,
300                    stream,
301                )?;
302                ops::dense_gemv(
303                    self.gpu.as_ref(),
304                    self.dense_gemv_kernel,
305                    hidden.offset(h as usize * 2),
306                    &self.lm_head_weight,
307                    logits.offset(v as usize * 2),
308                    v,
309                    h,
310                    stream,
311                )?;
312            }
313        } else if (3..=8).contains(&num_tokens)
314            && self.w4a16_batchm.kernel(num_tokens).0 != 0
315            && let Some(ref nvfp4) = self.lm_head_nvfp4
316        {
317            // K=3..8 verify lm_head: one weight read for all rows via the
318            // narrowest batched-GEMV tier covering the row count. nsys
319            // (2026-07-18, drafts=3 serve): the base M64-tile `w4a16_gemm`
320            // below cost 19.3 ms/verify-step on the [248320, 5120] NVFP4
321            // lm_head at M=4 — 94% of the M-tile is padding, ~33 GB/s
322            // effective. The batch GEMV streams the same 636 MB once at
323            // near-peak (~2.5 ms), the single largest slice of the K=4
324            // verify-vs-K=2 cost gap; the M=5..8 tiers extend that to the
325            // chain-verify rows (batchm_bench).
326            ops::w4a16_gemv_batchm(
327                self.gpu.as_ref(),
328                self.w4a16_batchm.kernel(num_tokens),
329                hidden,
330                nvfp4,
331                logits,
332                num_tokens,
333                v,
334                h,
335                stream,
336            )?;
337        } else if (9..=super::trait_impl::verify_e2::VERIFY_ROW_CAP as u32).contains(&num_tokens)
338            && lmhead_batched_wide_enabled()
339            && self.lm_head_batched_wide(hidden, num_tokens, logits, stream)?
340        {
341            // Handled by the wide arm; nothing launched when it returns false.
342        } else if let Some(ref nvfp4) = self.lm_head_nvfp4 {
343            ops::w4a16_gemm(
344                self.gpu.as_ref(),
345                self.w4a16_gemm_kernel,
346                hidden,
347                nvfp4,
348                logits,
349                num_tokens,
350                v,
351                h,
352                stream,
353            )?;
354        } else {
355            ops::dense_gemm(
356                self.gpu.as_ref(),
357                self.dense_gemm_kernel,
358                hidden,
359                &self.lm_head_weight,
360                logits,
361                num_tokens,
362                v,
363                h,
364                stream,
365            )?;
366        }
367        // Feature-2: overlay overridden logit columns AFTER the base projection,
368        // BEFORE softcap. Uniform-active route (seq_slot NULL); BF16 logits.
369        // No-op when no overlay is installed.
370        self.apply_lmhead_overlay(hidden, DevicePtr(0), logits, num_tokens, false, stream)?;
371        // Apply logit softcapping: logits = cap * tanh(logits / cap)
372        if self.logit_softcap_kernel.0 != 0 {
373            let cap = self.config.final_logit_softcapping;
374            let total = num_tokens * v;
375            self.apply_logit_softcap(logits, total, cap, stream)?;
376        }
377        Ok(logits)
378    }
379
380    /// Row range of the BF16 LM head this rank should compute, or `None` to keep the
381    /// replicated whole-vocab projection.
382    ///
383    /// `None` unless there is a real multi-rank communicator and the vocab divides evenly.
384    /// Kill switch: `AVAROK_NO_LMHEAD_VOCAB_TP=1`. Only reachable from the plain BF16 dense
385    /// head — the FP8 / NVFP4 / FP32-logits heads keep their existing path untouched.
386    fn lmhead_vocab_shard(&self, v: u32) -> Option<(usize, usize)> {
387        static OFF: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
388        if *OFF.get_or_init(|| std::env::var("AVAROK_NO_LMHEAD_VOCAB_TP").as_deref() == Ok("1")) {
389            return None;
390        }
391        let comm = self.comm_ref()?;
392        let ws = comm.world_size();
393        if ws < 2 || !(v as usize).is_multiple_of(ws) {
394            return None;
395        }
396        let len = v as usize / ws;
397        Some((comm.rank() * len, len))
398    }
399
400    pub(super) fn lm_head(&self, hidden: DevicePtr, stream: u64) -> Result<DevicePtr> {
401        let h = self.config.hidden_size as u32;
402        let v = self.config.vocab_size as u32;
403        // Pick the output buffer: FP32 scratch when use_fp32_logits is on,
404        // shared BF16 buffer otherwise. The sampler must use the matching
405        // dtype — see `decode_logits_dtype()`.
406        let (logits, fp32) = if self.use_fp32_logits {
407            (self.logits_fp32_buf, true)
408        } else {
409            (self.buffers.logits(), false)
410        };
411        if self.lm_head_q6k.is_some() {
412            // DeepSeek-V4.1: the GGUF's Q6_K head on its raw blocks
413            // (lm_head_q6k.rs). bf16 logits only; `use_fp32_logits` is off in
414            // production and this head has no FP32-output variant.
415            anyhow::ensure!(!fp32, "Q6_K lm_head has no FP32-logits variant");
416            self.lm_head_q6k_run(hidden, 1, logits, stream)?;
417            return Ok(logits);
418        }
419        if let Some(ref fp8) = self.lm_head_fp8 {
420            // FP8 E4M3 LM head (`--lm-head-dtype fp8`). `w8a16_gemv` has no
421            // FP32-output variant — it writes to whichever buffer is passed.
422            // `use_fp32_logits` is false in production, so `logits` is the
423            // shared BF16 buffer; the FP32-logits path is unused here.
424            ops::dense_gemv_fp8w(
425                self.gpu.as_ref(),
426                self.dense_gemv_fp8w_kernel,
427                hidden,
428                fp8,
429                logits,
430                v,
431                h,
432                stream,
433            )?;
434        } else if let Some(ref nvfp4) = self.lm_head_nvfp4 {
435            // Pick FP32-output variant when the FP32 logits buffer is the
436            // destination. Same packed-NVFP4 weights, same activation, but the
437            // accumulator is NOT downcast to BF16 — closes the 0.125-logit
438            // BF16-rounding tiebreak flip that triggers Gemma-4-31B's
439            // creative-collapse stop-word loop.
440            let kernel = if fp32 {
441                self.w4a16_gemv_logits_kernel
442            } else {
443                self.w4a16_gemv_kernel
444            };
445            ops::w4a16_gemv(
446                self.gpu.as_ref(),
447                kernel,
448                hidden,
449                nvfp4,
450                logits,
451                v,
452                h,
453                stream,
454            )?;
455        } else if fp32 {
456            // FP32-output dense GEMV: same precision-preservation reason as
457            // the NVFP4 variant above. Used when Gemma keeps the LM head
458            // as BF16 (skip_lm_head_quantization=true).
459            ops::dense_gemv(
460                self.gpu.as_ref(),
461                self.dense_gemv_fp32out_kernel,
462                hidden,
463                &self.lm_head_weight,
464                logits,
465                v,
466                h,
467                stream,
468            )?;
469        } else if let Some((begin, len)) = self.lmhead_vocab_shard(v) {
470            // VOCAB-PARALLEL BF16 head. The BF16 `lm_head` is REPLICATED on every rank —
471            // no loader shards it — so at TP=2 both ranks stream the whole vocab weight
472            // for the same logits. On GLM-5.3 that is 1.27 GB and **5.49 ms of an 79 ms
473            // decode step** (nsys, 2026-08-28), the single largest kernel after the KDA
474            // projections.
475            //
476            // Each rank computes a contiguous row range instead. This is BYTE-IDENTICAL
477            // by construction, not by tolerance: logit `n` is one full dot product over
478            // K=hidden run by the same kernel with the same reduction order — only WHICH
479            // rank runs it changes. The rest of the buffer is zeroed and the pieces are
480            // summed, and BF16 `x + 0` is exact, so the assembled vector is the same
481            // bytes the replicated head produced.
482            //
483            // 🪤 Correctness rests on `hidden` being bit-identical on every rank at this
484            // point. It is: the last thing the layer stack does is an all-reduce, which
485            // lands the same bytes everywhere. The byte-identity gate is what actually
486            // checks it — if that assumption ever breaks, the completions diverge.
487            self.gpu.memset_async(logits, 0, v as usize * 2, stream)?;
488            ops::dense_gemv(
489                self.gpu.as_ref(),
490                self.dense_gemv_kernel,
491                hidden,
492                &crate::weight_map::DenseWeight {
493                    weight: self.lm_head_weight.weight.offset(begin * h as usize * 2),
494                },
495                logits.offset(begin * 2),
496                len as u32,
497                h,
498                stream,
499            )?;
500            if let Some(comm) = self.comm_ref() {
501                comm.all_reduce_async(logits.0, v as usize * 2, stream)?;
502            }
503        } else {
504            ops::dense_gemv(
505                self.gpu.as_ref(),
506                self.dense_gemv_kernel,
507                hidden,
508                &self.lm_head_weight,
509                logits,
510                v,
511                h,
512                stream,
513            )?;
514        }
515        // Feature-2: overlay overridden logit columns AFTER the base projection,
516        // BEFORE softcap. Single-token; `fp32` selects the f32-logits kernel.
517        // No-op when no overlay is installed.
518        self.apply_lmhead_overlay(hidden, DevicePtr(0), logits, 1, fp32, stream)?;
519        // Apply logit softcapping: logits = cap * tanh(logits / cap)
520        if self.logit_softcap_kernel.0 != 0 || self.logit_softcap_fp32_kernel.0 != 0 {
521            let cap = self.config.final_logit_softcapping;
522            self.apply_logit_softcap_dtype(logits, v, cap, fp32, stream)?;
523        }
524        Ok(logits)
525    }
526
527    /// Apply logit softcapping in-place: `logits[i] = cap * tanh(logits[i] / cap)`.
528    /// BF16 path. Use `apply_logit_softcap_dtype` to dispatch by buffer dtype.
529    pub(super) fn apply_logit_softcap(
530        &self,
531        logits: DevicePtr,
532        num_elements: u32,
533        cap: f32,
534        stream: u64,
535    ) -> Result<()> {
536        use spark_runtime::kernel_args::KernelLaunch;
537        let inv_cap = 1.0f32 / cap;
538        KernelLaunch::new(self.gpu.as_ref(), self.logit_softcap_kernel)
539            .grid([num_elements.div_ceil(256), 1, 1])
540            .block([256, 1, 1])
541            .arg_ptr(logits)
542            .arg_u32(num_elements)
543            .arg_f32(inv_cap)
544            .arg_f32(cap)
545            .launch(stream)
546    }
547
548    /// Dtype-aware softcap dispatcher. Picks the BF16 or FP32 kernel based on
549    /// whether the buffer holds FP32 logits. No-op when softcap is disabled
550    /// (cap == 0). Used by the single-token decode `lm_head` to keep the FP32
551    /// path symmetrical when `use_fp32_logits` is on.
552    pub(super) fn apply_logit_softcap_dtype(
553        &self,
554        logits: DevicePtr,
555        num_elements: u32,
556        cap: f32,
557        is_fp32: bool,
558        stream: u64,
559    ) -> Result<()> {
560        use spark_runtime::kernel_args::KernelLaunch;
561        let kernel = if is_fp32 {
562            self.logit_softcap_fp32_kernel
563        } else {
564            self.logit_softcap_kernel
565        };
566        if kernel.0 == 0 {
567            return Ok(());
568        }
569        let inv_cap = 1.0f32 / cap;
570        KernelLaunch::new(self.gpu.as_ref(), kernel)
571            .grid([num_elements.div_ceil(256), 1, 1])
572            .block([256, 1, 1])
573            .arg_ptr(logits)
574            .arg_u32(num_elements)
575            .arg_f32(inv_cap)
576            .arg_f32(cap)
577            .launch(stream)
578    }
579
580    /// True when single-token decode `lm_head` writes FP32 logits to
581    /// `logits_fp32_buf`. Callers that consume those logits (sampler) MUST
582    /// read with the matching dtype. Prefill / batched-decode lm_head still
583    /// produce BF16, so this only applies to the `lm_head` (single-token)
584    /// return value.
585    pub fn decode_logits_fp32(&self) -> bool {
586        self.use_fp32_logits
587    }
588
589    /// Buffer pointer the single-token decode `lm_head` last wrote to. FP32
590    /// scratch when `use_fp32_logits`, otherwise the shared BF16 logits
591    /// buffer. Callers that previously hard-coded `self.buffers.logits()`
592    /// after `self.lm_head(...)` must use this so the sampler reads the
593    /// correct buffer dtype (the BF16 buffer is stale/empty in the FP32
594    /// path because lm_head writes elsewhere). Pair with
595    /// `logits_ptr_is_fp32` / `decode_logits_fp32` for dtype-aware reads.
596    pub fn decode_logits_ptr(&self) -> DevicePtr {
597        if self.use_fp32_logits {
598            self.logits_fp32_buf
599        } else {
600            self.buffers.logits()
601        }
602    }
603}