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}