spark_model/model/lm_head_q6k.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2// provenance-id: 526f6e616c6420522e205374657369616b
3//! The output head as raw Q6_K blocks, for the DeepSeek-V4.1 Flash GGUF.
4//!
5//! The Q2_K GGUF ships `output.weight` as Q6_K (0.5 GB). Expanding it to bf16
6//! at load made `dense_gemv_bf16` read 1.3 GB a token, 5.5 ms of a 65 ms
7//! decode step, the single largest kernel. Here the blocks stay as the file
8//! left them and the logits come from the vendored ggml `Q6_K x q8_1` GEMV
9//! (`kquant_mmvq_q6_k_w`, warp per vocab row), the same dot llama.cpp runs
10//! for this head: the hidden row is quantised to `block_q8_1` first, then one
11//! launch over the vocab. Installed by the factory only when the store holds
12//! `lm_head.weight` as [`WeightDtype::Q6K`]; every other head is untouched.
13//!
14//! [`WeightDtype::Q6K`]: spark_runtime::weights::WeightDtype::Q6K
15
16use anyhow::Result;
17use spark_runtime::gpu::{DevicePtr, KernelHandle};
18
19use super::types::TransformerModel;
20use crate::layers::ops;
21
22/// Rows of hidden state one `kquant_mmvq_*_w` launch takes (`KQ_MAX_M`).
23const MAX_M: u32 = 8;
24
25/// The resident Q6_K head and the two kernels that serve it.
26pub(crate) struct LmHeadQ6k {
27 /// `[vocab][hidden / 256]` raw 210-byte super-blocks.
28 blocks: DevicePtr,
29 mmvq_w: KernelHandle,
30 q8_rows: KernelHandle,
31 /// `block_q8_1 [MAX_M][hidden / 32]` scratch for the quantised hidden rows.
32 q8_scratch: DevicePtr,
33}
34
35impl TransformerModel {
36 /// Install the raw Q6_K head. `blocks` is the store's `lm_head.weight`
37 /// pointer, `[vocab, hidden]` in Q6_K super-blocks.
38 pub fn set_lm_head_q6k(&mut self, blocks: DevicePtr) -> Result<()> {
39 let h = self.config.hidden_size as u32;
40 let v = self.config.vocab_size as u32;
41 anyhow::ensure!(
42 h.is_multiple_of(256),
43 "Q6_K lm_head: hidden {h} is not a multiple of 256"
44 );
45 let mmvq_w = self.gpu.kernel(ops::KQUANT_MODULE, "kquant_mmvq_q6_k_w")?;
46 let q8_rows = self
47 .gpu
48 .kernel(ops::KQUANT_MODULE, "kquant_q8_1_rows_bf16")?;
49 let q8_scratch = self.gpu.alloc(ops::kquant_q8_1_rows_bytes(MAX_M, h))?;
50 tracing::info!(
51 "LM head served from raw Q6_K blocks (K-quant GEMV, vocab={v}, {} MB resident)",
52 ops::kquant_weight_bytes(v, h, ops::Q6K_BLOCK_BYTES) >> 20
53 );
54 self.lm_head_q6k = Some(LmHeadQ6k {
55 blocks,
56 mmvq_w,
57 q8_rows,
58 q8_scratch,
59 });
60 Ok(())
61 }
62
63 /// Logits for `m` bf16 hidden rows (`[m, hidden]`, contiguous) into
64 /// `logits` (`[m, vocab]` bf16) on the Q6_K head. `Ok(false)` when no
65 /// Q6_K head is installed, so the caller falls through to its own paths.
66 pub(super) fn lm_head_q6k_run(
67 &self,
68 hidden: DevicePtr,
69 m: u32,
70 logits: DevicePtr,
71 stream: u64,
72 ) -> Result<bool> {
73 let Some(ref q6) = self.lm_head_q6k else {
74 return Ok(false);
75 };
76 let h = self.config.hidden_size as u32;
77 let v = self.config.vocab_size as u32;
78 let gpu = self.gpu.as_ref();
79 let mut done = 0u32;
80 while done < m {
81 let mm = (m - done).min(MAX_M);
82 let x = hidden.offset(done as usize * h as usize * 2);
83 let out = logits.offset(done as usize * v as usize * 2);
84 ops::kquant_q8_1_rows(gpu, q6.q8_rows, x, q6.q8_scratch, mm, h, stream)?;
85 ops::kquant_mmvq_w(
86 gpu,
87 q6.mmvq_w,
88 q6.blocks,
89 q6.q8_scratch,
90 out,
91 v,
92 h,
93 mm,
94 stream,
95 )?;
96 done += mm;
97 }
98 Ok(true)
99 }
100}