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}