Skip to main content

combs_models/
qmatmul.rs

1//! Fused GGUF dequant-matmul CubeCL kernels: Q4_0, Q5_0, Q8_0 and the
2//! K-quants Q4_K, Q5_K, Q6_K — the formats real model files actually use.
3//!
4//! Weights stay packed at 4–6 bits in VRAM and are dequantized *inside*
5//! the matmul kernel — never materialized as f32. This is the memory win
6//! that lets a 7B Q4 model run in ~4 GB instead of ~28 GB of weight VRAM.
7//!
8//! Follows a two-layer design:
9//!
10//! - **Layout** (`repack_*` plus a per-format weight struct): GGUF
11//!   block streams are not word-aligned (18–210-byte blocks), so at
12//!   load we repack once into a GPU-friendly structure-of-arrays — packed
13//!   quant bytes as `u32` words plus `f32` super-scales (f16→f32 host
14//!   conversion is exact, keeping the kernels bit-comparable with the CPU
15//!   reference; re-packing scales to f16 pairs is a later small saving).
16//! - **Compute** (`*_dequant_kernel`, `*_matmul_kernel`): unpack, apply
17//!   scales, accumulate in f32. Each dequant-only kernel exists to
18//!   validate the layout bit-exactly against the harmony CPU reference
19//!   (`combs_formats::quants`); the fused matmuls are the production path.
20//!
21//! The portable fallback (dequantize at load + burn matmul) remains the
22//! default; these kernels are the opt-in fast path behind the linear seam.
23
24use core::marker::PhantomData;
25use std::sync::OnceLock;
26
27use cubecl::prelude::*;
28use cubecl::server::Handle;
29
30use crate::{ModelError, Result};
31
32/// Values per GGUF Q4_0 block.
33pub const Q4_0_BLOCK: usize = 32;
34/// Bytes per GGUF Q4_0 block: 2-byte f16 scale + 16 packed nibble bytes.
35pub const Q4_0_BLOCK_BYTES: usize = 18;
36
37/// Layout step: repack a raw GGUF Q4_0 block stream into the device layout
38/// the kernels consume — nibble bytes as little-endian `u32` words
39/// (4 words per block) and one `f32` scale per block. The f16→f32 scale
40/// conversion is exact, so no precision is lost relative to the reference.
41pub fn repack_q4_0(data: &[u8]) -> Result<(Vec<u32>, Vec<f32>)> {
42    if data.is_empty() || data.len() % Q4_0_BLOCK_BYTES != 0 {
43        return Err(ModelError::BadShape {
44            tensor: "q4_0 block stream".into(),
45            expected: vec![Q4_0_BLOCK_BYTES],
46            got: vec![data.len()],
47        });
48    }
49    let n_blocks = data.len() / Q4_0_BLOCK_BYTES;
50    let mut qs = Vec::with_capacity(n_blocks * 4);
51    let mut d = Vec::with_capacity(n_blocks);
52    for block in data.chunks_exact(Q4_0_BLOCK_BYTES) {
53        d.push(burn::tensor::f16::from_le_bytes([block[0], block[1]]).to_f32());
54        for w in 0..4 {
55            let o = 2 + 4 * w;
56            qs.push(u32::from_le_bytes([
57                block[o],
58                block[o + 1],
59                block[o + 2],
60                block[o + 3],
61            ]));
62        }
63    }
64    Ok((qs, d))
65}
66
67/// Dequantize-only kernel: `out[i]` = value `i` of the block stream, using
68/// the exact arithmetic of the CPU reference (`(nibble as i32 - 8) as f32
69/// * d`), so results are bit-identical. One thread per output value.
70#[cube(launch_unchecked)]
71fn q4_0_dequant_kernel(qs: &Array<u32>, d: &Array<f32>, out: &mut Array<f32>, n: usize) {
72    if ABSOLUTE_POS < n {
73        let block = ABSOLUTE_POS / 32;
74        let j = ABSOLUTE_POS % 32;
75        let byte_idx = j % 16;
76        let word = qs[block * 4 + byte_idx / 4];
77        let byte = (word >> (u32::cast_from(byte_idx % 4) * 8)) & 0xFF;
78        let mut nib = byte & 0xF;
79        if j >= 16 {
80            nib = byte >> 4;
81        }
82        out[ABSOLUTE_POS] = f32::cast_from(i32::cast_from(nib) - 8) * d[block];
83    }
84}
85
86/// Fused dequant-matmul: `out[row, col] = Σ_k x[row, k] · dequant(w[col, k])`
87/// for `x: [m, k]` f32 activations and `w: [n_out, k]` Q4_0 weights packed
88/// row-major with blocks along `k`. One thread per output element; per-block
89/// products accumulate unscaled and are multiplied by the block scale once
90/// (fewer multiplies, and the f32 accumulator never sees f16 range limits).
91#[cube(launch_unchecked)]
92fn q4_0_matmul_kernel(
93    x: &Array<f32>,
94    qs: &Array<u32>,
95    d: &Array<f32>,
96    out: &mut Array<f32>,
97    m: usize,
98    k: usize,
99    n_out: usize,
100) {
101    if ABSOLUTE_POS < m * n_out {
102        let row = ABSOLUTE_POS / n_out;
103        let col = ABSOLUTE_POS % n_out;
104        let blocks_per_row = k / 32;
105        let mut acc = 0.0f32;
106        for kb in 0..blocks_per_row {
107            let block = col * blocks_per_row + kb;
108            let x_base = row * k + kb * 32;
109            let mut block_acc = 0.0f32;
110            for w in 0..4usize {
111                let word = qs[block * 4 + w];
112                for b in 0..4usize {
113                    let byte = (word >> (u32::cast_from(b) * 8)) & 0xFF;
114                    let jj = w * 4 + b;
115                    let lo = f32::cast_from(i32::cast_from(byte & 0xF) - 8);
116                    let hi = f32::cast_from(i32::cast_from(byte >> 4) - 8);
117                    block_acc += lo * x[x_base + jj];
118                    block_acc += hi * x[x_base + 16 + jj];
119                }
120            }
121            acc += d[block] * block_acc;
122        }
123        out[row * n_out + col] = acc;
124    }
125}
126
127/// Threads per cube for the 1-D launches below.
128const CUBE_DIM: u32 = 256;
129
130/// Max cubes per grid dimension (wgpu/Metal limit).
131const MAX_CUBES_PER_DIM: u32 = 65535;
132
133fn cube_count_1d(total: u32) -> CubeCount {
134    CubeCount::Static(total.div_ceil(CUBE_DIM).max(1), 1, 1)
135}
136
137/// Like [`cube_count_1d`] but splits across the Y grid dimension when the
138/// thread count exceeds one dimension's limit (large prefill × vocab
139/// launches). Over-provisioned cubes are discarded by the in-kernel bound
140/// guard, which indexes by the linear `ABSOLUTE_POS`.
141fn cube_count_capped(total: u32) -> CubeCount {
142    let cubes = total.div_ceil(CUBE_DIM).max(1);
143    if cubes <= MAX_CUBES_PER_DIM {
144        CubeCount::Static(cubes, 1, 1)
145    } else {
146        let y = cubes.div_ceil(MAX_CUBES_PER_DIM);
147        CubeCount::Static(MAX_CUBES_PER_DIM, y, 1)
148    }
149}
150
151/// Grid for the tiled matmul kernels: one cube per (row, CUBE_DIM-wide
152/// column block) — X = column blocks, Y = rows. Both stay far under
153/// [`MAX_CUBES_PER_DIM`] for real weights (n_out ≤ 262k → ≤ 1024 column
154/// blocks; m is a prefill chunk).
155fn cube_count_tiled(n_out: u32, m: u32) -> CubeCount {
156    CubeCount::Static(n_out.div_ceil(CUBE_DIM).max(1), m.max(1), 1)
157}
158
159/// The tiled prefill kernels can be disabled with `COMBS_NO_TILED_MATMUL=1`
160/// (runtime A/B comparisons and triage); checked once per process.
161fn tiled_enabled() -> bool {
162    static DISABLED: OnceLock<bool> = OnceLock::new();
163    !*DISABLED.get_or_init(|| {
164        matches!(std::env::var("COMBS_NO_TILED_MATMUL").as_deref(), Ok("1"))
165    })
166}
167
168/// Runs the dequant-only kernel over a raw Q4_0 block stream. Exists for
169/// validation (bit-exact vs the CPU reference) and debugging, not the hot
170/// path.
171pub fn dequantize_q4_0_gpu<R: Runtime>(client: &ComputeClient<R>, data: &[u8]) -> Result<Vec<f32>> {
172    let (qs, d) = repack_q4_0(data)?;
173    let n = d.len() * Q4_0_BLOCK;
174    let qs_h = client.create_from_slice(u32::as_bytes(&qs));
175    let d_h = client.create_from_slice(f32::as_bytes(&d));
176    let out_h = client.empty(n * core::mem::size_of::<f32>());
177    unsafe {
178        q4_0_dequant_kernel::launch_unchecked::<R>(
179            client,
180            cube_count_1d(n as u32),
181            CubeDim::new_1d(CUBE_DIM),
182            ArrayArg::from_raw_parts(qs_h, qs.len()),
183            ArrayArg::from_raw_parts(d_h, d.len()),
184            ArrayArg::from_raw_parts(out_h.clone(), n),
185            n,
186        );
187    }
188    let bytes = client.read_one_unchecked(out_h);
189    Ok(f32::from_bytes(&bytes).to_vec())
190}
191
192/// A weight matrix resident in VRAM in packed Q4_0 form. `[n_out, k]`
193/// row-major, `k % 32 == 0`, blocks along `k` — exactly the GGUF tensor
194/// layout, so `from_gguf_bytes` takes the mmap'd tensor bytes unchanged.
195pub struct Q40Weight<R: Runtime> {
196    qs: Handle,
197    d: Handle,
198    n_out: usize,
199    k: usize,
200    _runtime: PhantomData<R>,
201}
202
203impl<R: Runtime> Q40Weight<R> {
204    /// Repacks a GGUF Q4_0 tensor onto the device. `data` is the raw block
205    /// stream for an `[n_out, k]` weight (the bytes `GgufSource` maps).
206    pub fn from_gguf_bytes(
207        client: &ComputeClient<R>,
208        data: &[u8],
209        n_out: usize,
210        k: usize,
211    ) -> Result<Self> {
212        if k == 0 || k % Q4_0_BLOCK != 0 || data.len() != n_out * k / Q4_0_BLOCK * Q4_0_BLOCK_BYTES
213        {
214            return Err(ModelError::BadShape {
215                tensor: "q4_0 weight".into(),
216                expected: vec![n_out, k / Q4_0_BLOCK.max(1) * Q4_0_BLOCK_BYTES],
217                got: vec![data.len()],
218            });
219        }
220        let (qs, d) = repack_q4_0(data)?;
221        Ok(Q40Weight {
222            qs: client.create_from_slice(u32::as_bytes(&qs)),
223            d: client.create_from_slice(f32::as_bytes(&d)),
224            n_out,
225            k,
226            _runtime: PhantomData,
227        })
228    }
229
230    /// Output features.
231    pub fn n_out(&self) -> usize {
232        self.n_out
233    }
234
235    /// Input features.
236    pub fn k(&self) -> usize {
237        self.k
238    }
239
240    /// Bytes this weight occupies in VRAM (packed nibbles + f32 scales) —
241    /// 20 bytes per 32 weights, vs 128 for f32 (6.4×) or 64 for f16 (3.2×).
242    pub fn vram_bytes(&self) -> usize {
243        let n_blocks = self.n_out * self.k / Q4_0_BLOCK;
244        n_blocks * (16 + core::mem::size_of::<f32>())
245    }
246
247    /// Device path: `y = x @ W^T` with `x` already resident as a contiguous
248    /// f32 buffer of `[m, k]`. Launch only — returns the output handle
249    /// (`[m, n_out]` f32) without any host round-trip.
250    pub fn matmul_device(&self, client: &ComputeClient<R>, x: Handle, m: usize) -> Handle {
251        let out_len = m * self.n_out;
252        let out_h = client.empty(out_len * core::mem::size_of::<f32>());
253        let n_blocks = self.n_out * self.k / Q4_0_BLOCK;
254        unsafe {
255            q4_0_matmul_kernel::launch_unchecked::<R>(
256                client,
257                cube_count_capped(out_len as u32),
258                CubeDim::new_1d(CUBE_DIM),
259                ArrayArg::from_raw_parts(x, m * self.k),
260                ArrayArg::from_raw_parts(self.qs.clone(), n_blocks * 4),
261                ArrayArg::from_raw_parts(self.d.clone(), n_blocks),
262                ArrayArg::from_raw_parts(out_h.clone(), out_len),
263                m,
264                self.k,
265                self.n_out,
266            );
267        }
268        out_h
269    }
270
271    /// `y = x @ W^T` for host-side `x: [m, k]`, returning `[m, n_out]`.
272    /// Host-slice convenience for tests/CLI probes.
273    pub fn matmul_host(&self, client: &ComputeClient<R>, x: &[f32], m: usize) -> Result<Vec<f32>> {
274        if m == 0 || x.len() != m * self.k {
275            return Err(ModelError::BadShape {
276                tensor: "q4_0 matmul input".into(),
277                expected: vec![m, self.k],
278                got: vec![x.len()],
279            });
280        }
281        let x_h = client.create_from_slice(f32::as_bytes(x));
282        let out_h = self.matmul_device(client, x_h, m);
283        let bytes = client.read_one_unchecked(out_h);
284        Ok(f32::from_bytes(&bytes).to_vec())
285    }
286}
287
288// ---------------------------------------------------------------------------
289// Q5_0 / Q8_0 (32-value blocks) — the formats ggml falls back to for
290// tensors whose row size is not a 256 multiple (e.g. SmolLM2's hidden 960),
291// so a "Q4_K_M" file of such a model is mostly Q5_0 with Q8_0 embeddings.
292// ---------------------------------------------------------------------------
293
294/// Bytes per GGUF Q5_0 block: f16 scale + u32 high bits + 16 nibble bytes.
295pub const Q5_0_BLOCK_BYTES: usize = 22;
296/// Bytes per GGUF Q8_0 block: f16 scale + 32 i8 values.
297pub const Q8_0_BLOCK_BYTES: usize = 34;
298
299/// Layout step for Q5_0: SoA of `(nibble words [4/blk], high-bit words
300/// [1/blk], f32 scales)` — 24 B / 32 weights = 6.0 bits/weight.
301pub fn repack_q5_0(data: &[u8]) -> Result<(Vec<u32>, Vec<u32>, Vec<f32>)> {
302    if data.is_empty() || data.len() % Q5_0_BLOCK_BYTES != 0 {
303        return Err(ModelError::BadShape {
304            tensor: "q5_0 block stream".into(),
305            expected: vec![Q5_0_BLOCK_BYTES],
306            got: vec![data.len()],
307        });
308    }
309    let n_blocks = data.len() / Q5_0_BLOCK_BYTES;
310    let mut qs = Vec::with_capacity(n_blocks * 4);
311    let mut qh = Vec::with_capacity(n_blocks);
312    let mut d = Vec::with_capacity(n_blocks);
313    for block in data.chunks_exact(Q5_0_BLOCK_BYTES) {
314        d.push(burn::tensor::f16::from_le_bytes([block[0], block[1]]).to_f32());
315        qh.push(u32::from_le_bytes([block[2], block[3], block[4], block[5]]));
316        for w in 0..4 {
317            let o = 6 + 4 * w;
318            qs.push(u32::from_le_bytes([
319                block[o],
320                block[o + 1],
321                block[o + 2],
322                block[o + 3],
323            ]));
324        }
325    }
326    Ok((qs, qh, d))
327}
328
329/// Layout step for Q8_0: SoA of `(i8 words [8/blk], f32 scales)` —
330/// 36 B / 32 weights = 9.0 bits/weight.
331pub fn repack_q8_0(data: &[u8]) -> Result<(Vec<u32>, Vec<f32>)> {
332    if data.is_empty() || data.len() % Q8_0_BLOCK_BYTES != 0 {
333        return Err(ModelError::BadShape {
334            tensor: "q8_0 block stream".into(),
335            expected: vec![Q8_0_BLOCK_BYTES],
336            got: vec![data.len()],
337        });
338    }
339    let n_blocks = data.len() / Q8_0_BLOCK_BYTES;
340    let mut qs = Vec::with_capacity(n_blocks * 8);
341    let mut d = Vec::with_capacity(n_blocks);
342    for block in data.chunks_exact(Q8_0_BLOCK_BYTES) {
343        d.push(burn::tensor::f16::from_le_bytes([block[0], block[1]]).to_f32());
344        for w in 0..8 {
345            let o = 2 + 4 * w;
346            qs.push(u32::from_le_bytes([
347                block[o],
348                block[o + 1],
349                block[o + 2],
350                block[o + 3],
351            ]));
352        }
353    }
354    Ok((qs, d))
355}
356
357/// Q5_0 dequant-only kernel, bit-exact mirror of the CPU reference:
358/// `((nibble | high_bit«4) − 16) · d`, high bit `j` of the block's u32 for
359/// value `j` (low nibbles), `j+16` for the highs.
360#[cube(launch_unchecked)]
361fn q5_0_dequant_kernel(
362    qs: &Array<u32>,
363    qh: &Array<u32>,
364    d: &Array<f32>,
365    out: &mut Array<f32>,
366    n: usize,
367) {
368    if ABSOLUTE_POS < n {
369        let block = ABSOLUTE_POS / 32;
370        let j = ABSOLUTE_POS % 32;
371        let byte_idx = j % 16;
372        let word = qs[block * 4 + byte_idx / 4];
373        let byte = (word >> (u32::cast_from(byte_idx % 4) * 8)) & 0xFF;
374        let mut nib = byte & 0xF;
375        if j >= 16 {
376            nib = byte >> 4;
377        }
378        let hi_bit = (qh[block] >> u32::cast_from(j)) & 1;
379        let q = i32::cast_from(nib | (hi_bit << 4)) - 16;
380        out[ABSOLUTE_POS] = f32::cast_from(q) * d[block];
381    }
382}
383
384/// Fused Q5_0 dequant-matmul (see `q4_0_matmul_kernel` for the scheme).
385#[cube(launch_unchecked)]
386fn q5_0_matmul_kernel(
387    x: &Array<f32>,
388    qs: &Array<u32>,
389    qh: &Array<u32>,
390    d: &Array<f32>,
391    out: &mut Array<f32>,
392    m: usize,
393    k: usize,
394    n_out: usize,
395) {
396    if ABSOLUTE_POS < m * n_out {
397        let row = ABSOLUTE_POS / n_out;
398        let col = ABSOLUTE_POS % n_out;
399        let blocks_per_row = k / 32;
400        let mut acc = 0.0f32;
401        for kb in 0..blocks_per_row {
402            let block = col * blocks_per_row + kb;
403            let x_base = row * k + kb * 32;
404            let bits = qh[block];
405            let mut block_acc = 0.0f32;
406            for w in 0..4usize {
407                let word = qs[block * 4 + w];
408                for b in 0..4usize {
409                    let byte = (word >> (u32::cast_from(b) * 8)) & 0xFF;
410                    let jj = w * 4 + b;
411                    let lo_bit = (bits >> u32::cast_from(jj)) & 1;
412                    let hi_bit = (bits >> u32::cast_from(jj + 16)) & 1;
413                    let lo = f32::cast_from(i32::cast_from((byte & 0xF) | (lo_bit << 4)) - 16);
414                    let hi = f32::cast_from(i32::cast_from((byte >> 4) | (hi_bit << 4)) - 16);
415                    block_acc += lo * x[x_base + jj];
416                    block_acc += hi * x[x_base + 16 + jj];
417                }
418            }
419            acc += d[block] * block_acc;
420        }
421        out[row * n_out + col] = acc;
422    }
423}
424
425/// Q8_0 dequant-only kernel: sign-extended i8 times the block scale.
426#[cube(launch_unchecked)]
427fn q8_0_dequant_kernel(qs: &Array<u32>, d: &Array<f32>, out: &mut Array<f32>, n: usize) {
428    if ABSOLUTE_POS < n {
429        let block = ABSOLUTE_POS / 32;
430        let j = ABSOLUTE_POS % 32;
431        let word = qs[block * 8 + j / 4];
432        let byte = (word >> (u32::cast_from(j % 4) * 8)) & 0xFF;
433        let q = (i32::cast_from(byte) << 24) >> 24;
434        out[ABSOLUTE_POS] = f32::cast_from(q) * d[block];
435    }
436}
437
438/// Fused Q8_0 dequant-matmul.
439#[cube(launch_unchecked)]
440fn q8_0_matmul_kernel(
441    x: &Array<f32>,
442    qs: &Array<u32>,
443    d: &Array<f32>,
444    out: &mut Array<f32>,
445    m: usize,
446    k: usize,
447    n_out: usize,
448) {
449    if ABSOLUTE_POS < m * n_out {
450        let row = ABSOLUTE_POS / n_out;
451        let col = ABSOLUTE_POS % n_out;
452        let blocks_per_row = k / 32;
453        let mut acc = 0.0f32;
454        for kb in 0..blocks_per_row {
455            let block = col * blocks_per_row + kb;
456            let x_base = row * k + kb * 32;
457            let mut block_acc = 0.0f32;
458            for w in 0..8usize {
459                let word = qs[block * 8 + w];
460                for b in 0..4usize {
461                    let byte = (word >> (u32::cast_from(b) * 8)) & 0xFF;
462                    let q = (i32::cast_from(byte) << 24) >> 24;
463                    block_acc += f32::cast_from(q) * x[x_base + w * 4 + b];
464                }
465            }
466            acc += d[block] * block_acc;
467        }
468        out[row * n_out + col] = acc;
469    }
470}
471
472/// Tiled variant of [`q8_0_matmul_kernel`] for m > 1: one cube per
473/// (row, CUBE_DIM-wide column block). Each 256-value k-tile of the row's
474/// activations is staged in shared memory once by the whole cube instead of
475/// being re-read from global memory by every column. Per-output arithmetic
476/// (ascending k, per-block sum then one scale multiply) is identical to the
477/// untiled kernel, so outputs are bit-identical; only the `x` load path
478/// changes. Both barriers sit outside the column guard: every thread of the
479/// cube reaches them even in a ragged final column block.
480#[cube(launch_unchecked)]
481fn q8_0_matmul_tiled_kernel(
482    x: &Array<f32>,
483    qs: &Array<u32>,
484    d: &Array<f32>,
485    out: &mut Array<f32>,
486    k: usize,
487    n_out: usize,
488) {
489    let mut staged = SharedMemory::<f32>::new(256usize);
490    let unit = UNIT_POS as usize;
491    let row = CUBE_POS_Y as usize;
492    let col = (CUBE_POS_X * CUBE_DIM + UNIT_POS) as usize;
493    let blocks_per_row = k / 32;
494    let n_tiles = (k + 255) / 256;
495    let mut acc = 0.0f32;
496    for t in 0..n_tiles {
497        let k0 = t * 256;
498        if k0 + unit < k {
499            staged[unit] = x[row * k + k0 + unit];
500        }
501        sync_cube();
502        if col < n_out {
503            let mut kb_end = (k0 + 256) / 32;
504            if blocks_per_row < kb_end {
505                kb_end = blocks_per_row;
506            }
507            for kb in (k0 / 32)..kb_end {
508                let block = col * blocks_per_row + kb;
509                let s_base = kb * 32 - k0;
510                let mut block_acc = 0.0f32;
511                for w in 0..8usize {
512                    let word = qs[block * 8 + w];
513                    for b in 0..4usize {
514                        let byte = (word >> (u32::cast_from(b) * 8)) & 0xFF;
515                        let q = (i32::cast_from(byte) << 24) >> 24;
516                        block_acc += f32::cast_from(q) * staged[s_base + w * 4 + b];
517                    }
518                }
519                acc += d[block] * block_acc;
520            }
521        }
522        sync_cube();
523    }
524    if col < n_out {
525        out[row * n_out + col] = acc;
526    }
527}
528
529/// Runs the Q5_0 dequant-only kernel (validation/debugging path).
530pub fn dequantize_q5_0_gpu<R: Runtime>(client: &ComputeClient<R>, data: &[u8]) -> Result<Vec<f32>> {
531    let (qs, qh, d) = repack_q5_0(data)?;
532    let n = d.len() * Q4_0_BLOCK;
533    let qs_h = client.create_from_slice(u32::as_bytes(&qs));
534    let qh_h = client.create_from_slice(u32::as_bytes(&qh));
535    let d_h = client.create_from_slice(f32::as_bytes(&d));
536    let out_h = client.empty(n * core::mem::size_of::<f32>());
537    unsafe {
538        q5_0_dequant_kernel::launch_unchecked::<R>(
539            client,
540            cube_count_1d(n as u32),
541            CubeDim::new_1d(CUBE_DIM),
542            ArrayArg::from_raw_parts(qs_h, qs.len()),
543            ArrayArg::from_raw_parts(qh_h, qh.len()),
544            ArrayArg::from_raw_parts(d_h, d.len()),
545            ArrayArg::from_raw_parts(out_h.clone(), n),
546            n,
547        );
548    }
549    let bytes = client.read_one_unchecked(out_h);
550    Ok(f32::from_bytes(&bytes).to_vec())
551}
552
553/// Runs the Q8_0 dequant-only kernel (validation/debugging path).
554pub fn dequantize_q8_0_gpu<R: Runtime>(client: &ComputeClient<R>, data: &[u8]) -> Result<Vec<f32>> {
555    let (qs, d) = repack_q8_0(data)?;
556    let n = d.len() * Q4_0_BLOCK;
557    let qs_h = client.create_from_slice(u32::as_bytes(&qs));
558    let d_h = client.create_from_slice(f32::as_bytes(&d));
559    let out_h = client.empty(n * core::mem::size_of::<f32>());
560    unsafe {
561        q8_0_dequant_kernel::launch_unchecked::<R>(
562            client,
563            cube_count_1d(n as u32),
564            CubeDim::new_1d(CUBE_DIM),
565            ArrayArg::from_raw_parts(qs_h, qs.len()),
566            ArrayArg::from_raw_parts(d_h, d.len()),
567            ArrayArg::from_raw_parts(out_h.clone(), n),
568            n,
569        );
570    }
571    let bytes = client.read_one_unchecked(out_h);
572    Ok(f32::from_bytes(&bytes).to_vec())
573}
574
575/// A weight matrix resident in VRAM in packed Q5_0 form (`[n_out, k]`,
576/// `k % 32 == 0`, blocks along `k`).
577pub struct Q50Weight<R: Runtime> {
578    qs: Handle,
579    qh: Handle,
580    d: Handle,
581    n_out: usize,
582    k: usize,
583    _runtime: PhantomData<R>,
584}
585
586impl<R: Runtime> Q50Weight<R> {
587    /// Repacks a GGUF Q5_0 tensor onto the device.
588    pub fn from_gguf_bytes(
589        client: &ComputeClient<R>,
590        data: &[u8],
591        n_out: usize,
592        k: usize,
593    ) -> Result<Self> {
594        if k == 0 || k % Q4_0_BLOCK != 0 || data.len() != n_out * k / Q4_0_BLOCK * Q5_0_BLOCK_BYTES
595        {
596            return Err(ModelError::BadShape {
597                tensor: "q5_0 weight".into(),
598                expected: vec![n_out, k],
599                got: vec![data.len()],
600            });
601        }
602        let (qs, qh, d) = repack_q5_0(data)?;
603        Ok(Q50Weight {
604            qs: client.create_from_slice(u32::as_bytes(&qs)),
605            qh: client.create_from_slice(u32::as_bytes(&qh)),
606            d: client.create_from_slice(f32::as_bytes(&d)),
607            n_out,
608            k,
609            _runtime: PhantomData,
610        })
611    }
612
613    /// Bytes in VRAM: 24 per 32 weights (6.0 bits/weight).
614    pub fn vram_bytes(&self) -> usize {
615        (self.n_out * self.k / Q4_0_BLOCK) * 24
616    }
617
618    /// Device path: launch only, output handle returned.
619    pub fn matmul_device(&self, client: &ComputeClient<R>, x: Handle, m: usize) -> Handle {
620        let out_len = m * self.n_out;
621        let out_h = client.empty(out_len * core::mem::size_of::<f32>());
622        let n_blocks = self.n_out * self.k / Q4_0_BLOCK;
623        unsafe {
624            q5_0_matmul_kernel::launch_unchecked::<R>(
625                client,
626                cube_count_capped(out_len as u32),
627                CubeDim::new_1d(CUBE_DIM),
628                ArrayArg::from_raw_parts(x, m * self.k),
629                ArrayArg::from_raw_parts(self.qs.clone(), n_blocks * 4),
630                ArrayArg::from_raw_parts(self.qh.clone(), n_blocks),
631                ArrayArg::from_raw_parts(self.d.clone(), n_blocks),
632                ArrayArg::from_raw_parts(out_h.clone(), out_len),
633                m,
634                self.k,
635                self.n_out,
636            );
637        }
638        out_h
639    }
640
641    /// Host-slice convenience for tests.
642    pub fn matmul_host(&self, client: &ComputeClient<R>, x: &[f32], m: usize) -> Result<Vec<f32>> {
643        if m == 0 || x.len() != m * self.k {
644            return Err(ModelError::BadShape {
645                tensor: "q5_0 matmul input".into(),
646                expected: vec![m, self.k],
647                got: vec![x.len()],
648            });
649        }
650        let x_h = client.create_from_slice(f32::as_bytes(x));
651        let out_h = self.matmul_device(client, x_h, m);
652        let bytes = client.read_one_unchecked(out_h);
653        Ok(f32::from_bytes(&bytes).to_vec())
654    }
655}
656
657/// A weight matrix resident in VRAM in packed Q8_0 form (`[n_out, k]`,
658/// `k % 32 == 0`, blocks along `k`).
659pub struct Q80Weight<R: Runtime> {
660    qs: Handle,
661    d: Handle,
662    n_out: usize,
663    k: usize,
664    _runtime: PhantomData<R>,
665}
666
667impl<R: Runtime> Q80Weight<R> {
668    /// Repacks a GGUF Q8_0 tensor onto the device.
669    pub fn from_gguf_bytes(
670        client: &ComputeClient<R>,
671        data: &[u8],
672        n_out: usize,
673        k: usize,
674    ) -> Result<Self> {
675        if k == 0 || k % Q4_0_BLOCK != 0 || data.len() != n_out * k / Q4_0_BLOCK * Q8_0_BLOCK_BYTES
676        {
677            return Err(ModelError::BadShape {
678                tensor: "q8_0 weight".into(),
679                expected: vec![n_out, k],
680                got: vec![data.len()],
681            });
682        }
683        let (qs, d) = repack_q8_0(data)?;
684        Ok(Q80Weight {
685            qs: client.create_from_slice(u32::as_bytes(&qs)),
686            d: client.create_from_slice(f32::as_bytes(&d)),
687            n_out,
688            k,
689            _runtime: PhantomData,
690        })
691    }
692
693    /// Bytes in VRAM: 36 per 32 weights (9.0 bits/weight).
694    pub fn vram_bytes(&self) -> usize {
695        (self.n_out * self.k / Q4_0_BLOCK) * 36
696    }
697
698    /// Device path: launch only, output handle returned. Decode (`m == 1`)
699    /// keeps the untiled kernel; prefill (`m > 1`) takes the shared-memory
700    /// tiled kernel unless `COMBS_NO_TILED_MATMUL=1`.
701    pub fn matmul_device(&self, client: &ComputeClient<R>, x: Handle, m: usize) -> Handle {
702        self.matmul_device_with(client, x, m, m > 1 && tiled_enabled())
703    }
704
705    /// Launch with an explicit kernel choice (the parity tests compare
706    /// tiled vs untiled on identical inputs).
707    pub(crate) fn matmul_device_with(
708        &self,
709        client: &ComputeClient<R>,
710        x: Handle,
711        m: usize,
712        tiled: bool,
713    ) -> Handle {
714        let out_len = m * self.n_out;
715        let out_h = client.empty(out_len * core::mem::size_of::<f32>());
716        let n_blocks = self.n_out * self.k / Q4_0_BLOCK;
717        if tiled {
718            unsafe {
719                q8_0_matmul_tiled_kernel::launch_unchecked::<R>(
720                    client,
721                    cube_count_tiled(self.n_out as u32, m as u32),
722                    CubeDim::new_1d(CUBE_DIM),
723                    ArrayArg::from_raw_parts(x, m * self.k),
724                    ArrayArg::from_raw_parts(self.qs.clone(), n_blocks * 8),
725                    ArrayArg::from_raw_parts(self.d.clone(), n_blocks),
726                    ArrayArg::from_raw_parts(out_h.clone(), out_len),
727                    self.k,
728                    self.n_out,
729                );
730            }
731        } else {
732            unsafe {
733                q8_0_matmul_kernel::launch_unchecked::<R>(
734                    client,
735                    cube_count_capped(out_len as u32),
736                    CubeDim::new_1d(CUBE_DIM),
737                    ArrayArg::from_raw_parts(x, m * self.k),
738                    ArrayArg::from_raw_parts(self.qs.clone(), n_blocks * 8),
739                    ArrayArg::from_raw_parts(self.d.clone(), n_blocks),
740                    ArrayArg::from_raw_parts(out_h.clone(), out_len),
741                    m,
742                    self.k,
743                    self.n_out,
744                );
745            }
746        }
747        out_h
748    }
749
750    /// Host-slice convenience for tests.
751    pub fn matmul_host(&self, client: &ComputeClient<R>, x: &[f32], m: usize) -> Result<Vec<f32>> {
752        if m == 0 || x.len() != m * self.k {
753            return Err(ModelError::BadShape {
754                tensor: "q8_0 matmul input".into(),
755                expected: vec![m, self.k],
756                got: vec![x.len()],
757            });
758        }
759        let x_h = client.create_from_slice(f32::as_bytes(x));
760        let out_h = self.matmul_device(client, x_h, m);
761        let bytes = client.read_one_unchecked(out_h);
762        Ok(f32::from_bytes(&bytes).to_vec())
763    }
764}
765
766// ---------------------------------------------------------------------------
767// K-quants (256-value superblocks). Shared in-kernel byte helpers first.
768// ---------------------------------------------------------------------------
769
770/// Values per K-quant superblock.
771pub const K_SUPERBLOCK: usize = 256;
772/// Bytes per GGUF Q4_K superblock: f16 d + f16 dmin + 12B scales + 128B quants.
773pub const Q4_K_BLOCK_BYTES: usize = 144;
774/// Bytes per GGUF Q5_K superblock: Q4_K's layout + 32B high bits.
775pub const Q5_K_BLOCK_BYTES: usize = 176;
776/// Bytes per GGUF Q6_K superblock: 128B ql + 64B qh + 16 i8 scales + f16 d.
777pub const Q6_K_BLOCK_BYTES: usize = 210;
778
779/// Reads byte `idx` from a byte stream stored as little-endian u32 words.
780#[cube]
781fn byte_at(words: &Array<u32>, idx: usize) -> u32 {
782    (words[idx / 4] >> (u32::cast_from(idx % 4) * 8)) & 0xFF
783}
784
785/// Sign-extends byte `idx` of a word-packed stream as an i8.
786#[cube]
787fn i8_at(words: &Array<u32>, idx: usize) -> i32 {
788    (i32::cast_from(byte_at(words, idx)) << 24) >> 24
789}
790
791/// ggml `get_scale_min_k4`, scale half: 6-bit scale of sub-block `j` from
792/// the 12 packed bytes starting at `base` (top 2 bits of bytes 0..4 carry
793/// the high bits of sub-blocks 4..8).
794#[cube]
795fn k4_scale(scales: &Array<u32>, base: usize, j: usize) -> u32 {
796    let mut v = 0u32;
797    if j < 4 {
798        v = byte_at(scales, base + j) & 63;
799    } else {
800        v = (byte_at(scales, base + j + 4) & 0xF) | ((byte_at(scales, base + j - 4) >> 6) << 4);
801    }
802    v
803}
804
805/// ggml `get_scale_min_k4`, min half.
806#[cube]
807fn k4_min(scales: &Array<u32>, base: usize, j: usize) -> u32 {
808    let mut v = 0u32;
809    if j < 4 {
810        v = byte_at(scales, base + j + 4) & 63;
811    } else {
812        v = (byte_at(scales, base + j + 4) >> 4) | ((byte_at(scales, base + j) >> 6) << 4);
813    }
814    v
815}
816
817/// Layout step for Q4_K: split each 144-byte superblock into SoA device
818/// arrays — `(qs words, [d, dmin] f32 pairs, scale words)`. 148 B per 256
819/// weights = 4.63 bits/weight (GGUF native is 4.5).
820pub fn repack_q4_k(data: &[u8]) -> Result<(Vec<u32>, Vec<f32>, Vec<u32>)> {
821    if data.is_empty() || data.len() % Q4_K_BLOCK_BYTES != 0 {
822        return Err(ModelError::BadShape {
823            tensor: "q4_k superblock stream".into(),
824            expected: vec![Q4_K_BLOCK_BYTES],
825            got: vec![data.len()],
826        });
827    }
828    let n_sb = data.len() / Q4_K_BLOCK_BYTES;
829    let mut qs = Vec::with_capacity(n_sb * 32);
830    let mut dd = Vec::with_capacity(n_sb * 2);
831    let mut scales = Vec::with_capacity(n_sb * 3);
832    for sb in data.chunks_exact(Q4_K_BLOCK_BYTES) {
833        dd.push(burn::tensor::f16::from_le_bytes([sb[0], sb[1]]).to_f32());
834        dd.push(burn::tensor::f16::from_le_bytes([sb[2], sb[3]]).to_f32());
835        for w in 0..3 {
836            let o = 4 + 4 * w;
837            scales.push(u32::from_le_bytes([sb[o], sb[o + 1], sb[o + 2], sb[o + 3]]));
838        }
839        for w in 0..32 {
840            let o = 16 + 4 * w;
841            qs.push(u32::from_le_bytes([sb[o], sb[o + 1], sb[o + 2], sb[o + 3]]));
842        }
843    }
844    Ok((qs, dd, scales))
845}
846
847/// Q4_K dequant-only kernel, arithmetic mirrored from the CPU reference:
848/// `out = (d·sc) · q - (dmin·m)` per 32-value sub-block.
849#[cube(launch_unchecked)]
850fn q4_k_dequant_kernel(
851    qs: &Array<u32>,
852    dd: &Array<f32>,
853    scales: &Array<u32>,
854    out: &mut Array<f32>,
855    n: usize,
856) {
857    if ABSOLUTE_POS < n {
858        let sb = ABSOLUTE_POS / 256;
859        let r = ABSOLUTE_POS % 256;
860        let j = r / 64; // 64-value group: 32 low-nibble values then 32 high
861        let t = (r % 64) / 32; // 0 = low nibble, 1 = high nibble
862        let l = r % 32;
863        let byte = byte_at(qs, sb * 128 + j * 32 + l);
864        let mut q = byte & 0xF;
865        if t == 1 {
866            q = byte >> 4;
867        }
868        let sidx = 2 * j + t;
869        let sc = k4_scale(scales, sb * 12, sidx);
870        let mn = k4_min(scales, sb * 12, sidx);
871        let d1 = dd[sb * 2] * f32::cast_from(sc);
872        let fmin = dd[sb * 2 + 1] * f32::cast_from(mn);
873        out[ABSOLUTE_POS] = d1 * f32::cast_from(q) - fmin;
874    }
875}
876
877/// Fused Q4_K dequant-matmul. Uses the ggml sum-split: within a sub-block,
878/// `Σ (d·sc·q − dmin·m)·x = d·sc·Σ q·x − dmin·m·Σ x`, so the packed bytes
879/// are touched once and the scales applied once per 32 values.
880#[cube(launch_unchecked)]
881fn q4_k_matmul_kernel(
882    x: &Array<f32>,
883    qs: &Array<u32>,
884    dd: &Array<f32>,
885    scales: &Array<u32>,
886    out: &mut Array<f32>,
887    m: usize,
888    k: usize,
889    n_out: usize,
890) {
891    if ABSOLUTE_POS < m * n_out {
892        let row = ABSOLUTE_POS / n_out;
893        let col = ABSOLUTE_POS % n_out;
894        let sb_per_row = k / 256;
895        let mut acc = 0.0f32;
896        for sbi in 0..sb_per_row {
897            let sb = col * sb_per_row + sbi;
898            let d = dd[sb * 2];
899            let dmin = dd[sb * 2 + 1];
900            let s_base = sb * 12;
901            let x_base = row * k + sbi * 256;
902            for j in 0..4usize {
903                let mut sum_lo = 0.0f32;
904                let mut sum_hi = 0.0f32;
905                let mut xs_lo = 0.0f32;
906                let mut xs_hi = 0.0f32;
907                for w in 0..8usize {
908                    let word = qs[sb * 32 + j * 8 + w];
909                    for b in 0..4usize {
910                        let byte = (word >> (u32::cast_from(b) * 8)) & 0xFF;
911                        let l = 4 * w + b;
912                        let x1 = x[x_base + 64 * j + l];
913                        let x2 = x[x_base + 64 * j + 32 + l];
914                        sum_lo += f32::cast_from(byte & 0xF) * x1;
915                        sum_hi += f32::cast_from(byte >> 4) * x2;
916                        xs_lo += x1;
917                        xs_hi += x2;
918                    }
919                }
920                let sc1 = f32::cast_from(k4_scale(scales, s_base, 2 * j));
921                let mn1 = f32::cast_from(k4_min(scales, s_base, 2 * j));
922                let sc2 = f32::cast_from(k4_scale(scales, s_base, 2 * j + 1));
923                let mn2 = f32::cast_from(k4_min(scales, s_base, 2 * j + 1));
924                acc += d * sc1 * sum_lo - dmin * mn1 * xs_lo;
925                acc += d * sc2 * sum_hi - dmin * mn2 * xs_hi;
926            }
927        }
928        out[row * n_out + col] = acc;
929    }
930}
931
932/// Tiled variant of [`q4_k_matmul_kernel`] for m > 1: the 256-value
933/// K-superblock is exactly one shared-memory tile (`k % 256 == 0` always
934/// holds for K-quants, so there is no ragged tail). The cube stages the
935/// superblock's activation slice once, barriers, and every column applies
936/// the same sum-split in the same ascending-k order as the untiled kernel —
937/// outputs are bit-identical; only the `x` load path changes.
938#[cube(launch_unchecked)]
939fn q4_k_matmul_tiled_kernel(
940    x: &Array<f32>,
941    qs: &Array<u32>,
942    dd: &Array<f32>,
943    scales: &Array<u32>,
944    out: &mut Array<f32>,
945    k: usize,
946    n_out: usize,
947) {
948    let mut staged = SharedMemory::<f32>::new(256usize);
949    let unit = UNIT_POS as usize;
950    let row = CUBE_POS_Y as usize;
951    let col = (CUBE_POS_X * CUBE_DIM + UNIT_POS) as usize;
952    let sb_per_row = k / 256;
953    let mut acc = 0.0f32;
954    for sbi in 0..sb_per_row {
955        staged[unit] = x[row * k + sbi * 256 + unit];
956        sync_cube();
957        if col < n_out {
958            let sb = col * sb_per_row + sbi;
959            let d = dd[sb * 2];
960            let dmin = dd[sb * 2 + 1];
961            let s_base = sb * 12;
962            for j in 0..4usize {
963                let mut sum_lo = 0.0f32;
964                let mut sum_hi = 0.0f32;
965                let mut xs_lo = 0.0f32;
966                let mut xs_hi = 0.0f32;
967                for w in 0..8usize {
968                    let word = qs[sb * 32 + j * 8 + w];
969                    for b in 0..4usize {
970                        let byte = (word >> (u32::cast_from(b) * 8)) & 0xFF;
971                        let l = 4 * w + b;
972                        let x1 = staged[64 * j + l];
973                        let x2 = staged[64 * j + 32 + l];
974                        sum_lo += f32::cast_from(byte & 0xF) * x1;
975                        sum_hi += f32::cast_from(byte >> 4) * x2;
976                        xs_lo += x1;
977                        xs_hi += x2;
978                    }
979                }
980                let sc1 = f32::cast_from(k4_scale(scales, s_base, 2 * j));
981                let mn1 = f32::cast_from(k4_min(scales, s_base, 2 * j));
982                let sc2 = f32::cast_from(k4_scale(scales, s_base, 2 * j + 1));
983                let mn2 = f32::cast_from(k4_min(scales, s_base, 2 * j + 1));
984                acc += d * sc1 * sum_lo - dmin * mn1 * xs_lo;
985                acc += d * sc2 * sum_hi - dmin * mn2 * xs_hi;
986            }
987        }
988        sync_cube();
989    }
990    if col < n_out {
991        out[row * n_out + col] = acc;
992    }
993}
994
995/// Layout step for Q5_K: split each 176-byte superblock into SoA device
996/// arrays — `(qs words, qh words, [d, dmin] f32 pairs, scale words)`.
997/// 180 B per 256 weights = 5.63 bits/weight (GGUF native is 5.5).
998pub fn repack_q5_k(data: &[u8]) -> Result<(Vec<u32>, Vec<u32>, Vec<f32>, Vec<u32>)> {
999    if data.is_empty() || data.len() % Q5_K_BLOCK_BYTES != 0 {
1000        return Err(ModelError::BadShape {
1001            tensor: "q5_k superblock stream".into(),
1002            expected: vec![Q5_K_BLOCK_BYTES],
1003            got: vec![data.len()],
1004        });
1005    }
1006    let n_sb = data.len() / Q5_K_BLOCK_BYTES;
1007    let word = |sb: &[u8], o: usize| u32::from_le_bytes([sb[o], sb[o + 1], sb[o + 2], sb[o + 3]]);
1008    let mut qs = Vec::with_capacity(n_sb * 32);
1009    let mut qh = Vec::with_capacity(n_sb * 8);
1010    let mut dd = Vec::with_capacity(n_sb * 2);
1011    let mut scales = Vec::with_capacity(n_sb * 3);
1012    for sb in data.chunks_exact(Q5_K_BLOCK_BYTES) {
1013        dd.push(burn::tensor::f16::from_le_bytes([sb[0], sb[1]]).to_f32());
1014        dd.push(burn::tensor::f16::from_le_bytes([sb[2], sb[3]]).to_f32());
1015        for w in 0..3 {
1016            scales.push(word(sb, 4 + 4 * w));
1017        }
1018        for w in 0..8 {
1019            qh.push(word(sb, 16 + 4 * w));
1020        }
1021        for w in 0..32 {
1022            qs.push(word(sb, 48 + 4 * w));
1023        }
1024    }
1025    Ok((qs, qh, dd, scales))
1026}
1027
1028/// Q5_K dequant-only kernel, arithmetic mirrored from the CPU reference:
1029/// Q4_K plus the high-bit plane — group `j` reads bit `2j + t` of `qh[l]`
1030/// and the value is `(d·sc) · (nib | hi«4) - (dmin·m)`.
1031#[cube(launch_unchecked)]
1032fn q5_k_dequant_kernel(
1033    qs: &Array<u32>,
1034    qh: &Array<u32>,
1035    dd: &Array<f32>,
1036    scales: &Array<u32>,
1037    out: &mut Array<f32>,
1038    n: usize,
1039) {
1040    if ABSOLUTE_POS < n {
1041        let sb = ABSOLUTE_POS / 256;
1042        let r = ABSOLUTE_POS % 256;
1043        let j = r / 64; // 64-value group: 32 low-nibble values then 32 high
1044        let t = (r % 64) / 32; // 0 = low nibble, 1 = high nibble
1045        let l = r % 32;
1046        let byte = byte_at(qs, sb * 128 + j * 32 + l);
1047        let mut nib = byte & 0xF;
1048        if t == 1 {
1049            nib = byte >> 4;
1050        }
1051        let hi = (byte_at(qh, sb * 32 + l) >> u32::cast_from(2 * j + t)) & 1;
1052        let sidx = 2 * j + t;
1053        let sc = k4_scale(scales, sb * 12, sidx);
1054        let mn = k4_min(scales, sb * 12, sidx);
1055        let d1 = dd[sb * 2] * f32::cast_from(sc);
1056        let fmin = dd[sb * 2 + 1] * f32::cast_from(mn);
1057        out[ABSOLUTE_POS] = d1 * f32::cast_from(nib | (hi << 4)) - fmin;
1058    }
1059}
1060
1061/// Fused Q5_K dequant-matmul: the Q4_K sum-split with the 5th bit folded
1062/// into `q` before the multiply.
1063#[cube(launch_unchecked)]
1064fn q5_k_matmul_kernel(
1065    x: &Array<f32>,
1066    qs: &Array<u32>,
1067    qh: &Array<u32>,
1068    dd: &Array<f32>,
1069    scales: &Array<u32>,
1070    out: &mut Array<f32>,
1071    m: usize,
1072    k: usize,
1073    n_out: usize,
1074) {
1075    if ABSOLUTE_POS < m * n_out {
1076        let row = ABSOLUTE_POS / n_out;
1077        let col = ABSOLUTE_POS % n_out;
1078        let sb_per_row = k / 256;
1079        let mut acc = 0.0f32;
1080        for sbi in 0..sb_per_row {
1081            let sb = col * sb_per_row + sbi;
1082            let d = dd[sb * 2];
1083            let dmin = dd[sb * 2 + 1];
1084            let s_base = sb * 12;
1085            let x_base = row * k + sbi * 256;
1086            for j in 0..4usize {
1087                let mut sum_lo = 0.0f32;
1088                let mut sum_hi = 0.0f32;
1089                let mut xs_lo = 0.0f32;
1090                let mut xs_hi = 0.0f32;
1091                for w in 0..8usize {
1092                    let word = qs[sb * 32 + j * 8 + w];
1093                    for b in 0..4usize {
1094                        let byte = (word >> (u32::cast_from(b) * 8)) & 0xFF;
1095                        let l = 4 * w + b;
1096                        let hb = byte_at(qh, sb * 32 + l);
1097                        let hi_lo = (hb >> u32::cast_from(2 * j)) & 1;
1098                        let hi_hi = (hb >> u32::cast_from(2 * j + 1)) & 1;
1099                        let x1 = x[x_base + 64 * j + l];
1100                        let x2 = x[x_base + 64 * j + 32 + l];
1101                        sum_lo += f32::cast_from((byte & 0xF) | (hi_lo << 4)) * x1;
1102                        sum_hi += f32::cast_from((byte >> 4) | (hi_hi << 4)) * x2;
1103                        xs_lo += x1;
1104                        xs_hi += x2;
1105                    }
1106                }
1107                let sc1 = f32::cast_from(k4_scale(scales, s_base, 2 * j));
1108                let mn1 = f32::cast_from(k4_min(scales, s_base, 2 * j));
1109                let sc2 = f32::cast_from(k4_scale(scales, s_base, 2 * j + 1));
1110                let mn2 = f32::cast_from(k4_min(scales, s_base, 2 * j + 1));
1111                acc += d * sc1 * sum_lo - dmin * mn1 * xs_lo;
1112                acc += d * sc2 * sum_hi - dmin * mn2 * xs_hi;
1113            }
1114        }
1115        out[row * n_out + col] = acc;
1116    }
1117}
1118
1119/// Layout step for Q6_K: split each 210-byte superblock into SoA device
1120/// arrays — `(ql words, qh words, i8 scale words, d f32)`. 212 B per 256
1121/// weights = 6.63 bits/weight (GGUF native is 6.56).
1122pub fn repack_q6_k(data: &[u8]) -> Result<(Vec<u32>, Vec<u32>, Vec<u32>, Vec<f32>)> {
1123    if data.is_empty() || data.len() % Q6_K_BLOCK_BYTES != 0 {
1124        return Err(ModelError::BadShape {
1125            tensor: "q6_k superblock stream".into(),
1126            expected: vec![Q6_K_BLOCK_BYTES],
1127            got: vec![data.len()],
1128        });
1129    }
1130    let n_sb = data.len() / Q6_K_BLOCK_BYTES;
1131    let word = |sb: &[u8], o: usize| u32::from_le_bytes([sb[o], sb[o + 1], sb[o + 2], sb[o + 3]]);
1132    let mut ql = Vec::with_capacity(n_sb * 32);
1133    let mut qh = Vec::with_capacity(n_sb * 16);
1134    let mut sc = Vec::with_capacity(n_sb * 4);
1135    let mut d = Vec::with_capacity(n_sb);
1136    for sb in data.chunks_exact(Q6_K_BLOCK_BYTES) {
1137        for w in 0..32 {
1138            ql.push(word(sb, 4 * w));
1139        }
1140        for w in 0..16 {
1141            qh.push(word(sb, 128 + 4 * w));
1142        }
1143        for w in 0..4 {
1144            sc.push(word(sb, 192 + 4 * w));
1145        }
1146        d.push(burn::tensor::f16::from_le_bytes([sb[208], sb[209]]).to_f32());
1147    }
1148    Ok((ql, qh, sc, d))
1149}
1150
1151/// Q6_K dequant-only kernel, mirroring the CPU reference: each 128-value
1152/// half yields quadrants t 0..4 with `q = (ql nibble) | (qh 2-bit « 4)`,
1153/// biased −32, times `d · scales[i8]`.
1154#[cube(launch_unchecked)]
1155fn q6_k_dequant_kernel(
1156    ql: &Array<u32>,
1157    qh: &Array<u32>,
1158    sc: &Array<u32>,
1159    d: &Array<f32>,
1160    out: &mut Array<f32>,
1161    n: usize,
1162) {
1163    if ABSOLUTE_POS < n {
1164        let sb = ABSOLUTE_POS / 256;
1165        let r = ABSOLUTE_POS % 256;
1166        let half = r / 128;
1167        let t = (r % 128) / 32; // quadrant within the half
1168        let l = r % 32;
1169        let ql_byte = byte_at(ql, sb * 128 + half * 64 + (t % 2) * 32 + l);
1170        let mut nib = ql_byte & 0xF;
1171        if t >= 2 {
1172            nib = ql_byte >> 4;
1173        }
1174        let hi = (byte_at(qh, sb * 64 + half * 32 + l) >> (u32::cast_from(t) * 2)) & 3;
1175        let q = i32::cast_from(nib | (hi << 4)) - 32;
1176        let scale = i8_at(sc, sb * 16 + half * 8 + l / 16 + 2 * t);
1177        out[ABSOLUTE_POS] = d[sb] * f32::cast_from(scale) * f32::cast_from(q);
1178    }
1179}
1180
1181/// Fused Q6_K dequant-matmul: per 16-value scale group,
1182/// `acc += d · sc · Σ (q − 32) · x`.
1183#[cube(launch_unchecked)]
1184fn q6_k_matmul_kernel(
1185    x: &Array<f32>,
1186    ql: &Array<u32>,
1187    qh: &Array<u32>,
1188    sc: &Array<u32>,
1189    d: &Array<f32>,
1190    out: &mut Array<f32>,
1191    m: usize,
1192    k: usize,
1193    n_out: usize,
1194) {
1195    if ABSOLUTE_POS < m * n_out {
1196        let row = ABSOLUTE_POS / n_out;
1197        let col = ABSOLUTE_POS % n_out;
1198        let sb_per_row = k / 256;
1199        let mut acc = 0.0f32;
1200        for sbi in 0..sb_per_row {
1201            let sb = col * sb_per_row + sbi;
1202            let dsb = d[sb];
1203            let x_base = row * k + sbi * 256;
1204            for half in 0..2usize {
1205                for t in 0..4usize {
1206                    for g in 0..2usize {
1207                        let mut sum = 0.0f32;
1208                        for l0 in 0..16usize {
1209                            let l = g * 16 + l0;
1210                            let ql_byte = byte_at(ql, sb * 128 + half * 64 + (t % 2) * 32 + l);
1211                            let mut nib = ql_byte & 0xF;
1212                            if t >= 2 {
1213                                nib = ql_byte >> 4;
1214                            }
1215                            let hi =
1216                                (byte_at(qh, sb * 64 + half * 32 + l) >> (u32::cast_from(t) * 2))
1217                                    & 3;
1218                            let q = i32::cast_from(nib | (hi << 4)) - 32;
1219                            sum += f32::cast_from(q) * x[x_base + half * 128 + t * 32 + l];
1220                        }
1221                        let scale = i8_at(sc, sb * 16 + half * 8 + g + 2 * t);
1222                        acc += dsb * f32::cast_from(scale) * sum;
1223                    }
1224                }
1225            }
1226        }
1227        out[row * n_out + col] = acc;
1228    }
1229}
1230
1231/// Runs the Q4_K dequant-only kernel (validation/debugging path).
1232pub fn dequantize_q4_k_gpu<R: Runtime>(client: &ComputeClient<R>, data: &[u8]) -> Result<Vec<f32>> {
1233    let (qs, dd, scales) = repack_q4_k(data)?;
1234    let n = (dd.len() / 2) * K_SUPERBLOCK;
1235    let qs_h = client.create_from_slice(u32::as_bytes(&qs));
1236    let dd_h = client.create_from_slice(f32::as_bytes(&dd));
1237    let sc_h = client.create_from_slice(u32::as_bytes(&scales));
1238    let out_h = client.empty(n * core::mem::size_of::<f32>());
1239    unsafe {
1240        q4_k_dequant_kernel::launch_unchecked::<R>(
1241            client,
1242            cube_count_1d(n as u32),
1243            CubeDim::new_1d(CUBE_DIM),
1244            ArrayArg::from_raw_parts(qs_h, qs.len()),
1245            ArrayArg::from_raw_parts(dd_h, dd.len()),
1246            ArrayArg::from_raw_parts(sc_h, scales.len()),
1247            ArrayArg::from_raw_parts(out_h.clone(), n),
1248            n,
1249        );
1250    }
1251    let bytes = client.read_one_unchecked(out_h);
1252    Ok(f32::from_bytes(&bytes).to_vec())
1253}
1254
1255/// Runs the Q5_K dequant-only kernel (validation/debugging path).
1256pub fn dequantize_q5_k_gpu<R: Runtime>(client: &ComputeClient<R>, data: &[u8]) -> Result<Vec<f32>> {
1257    let (qs, qh, dd, scales) = repack_q5_k(data)?;
1258    let n = (dd.len() / 2) * K_SUPERBLOCK;
1259    let qs_h = client.create_from_slice(u32::as_bytes(&qs));
1260    let qh_h = client.create_from_slice(u32::as_bytes(&qh));
1261    let dd_h = client.create_from_slice(f32::as_bytes(&dd));
1262    let sc_h = client.create_from_slice(u32::as_bytes(&scales));
1263    let out_h = client.empty(n * core::mem::size_of::<f32>());
1264    unsafe {
1265        q5_k_dequant_kernel::launch_unchecked::<R>(
1266            client,
1267            cube_count_1d(n as u32),
1268            CubeDim::new_1d(CUBE_DIM),
1269            ArrayArg::from_raw_parts(qs_h, qs.len()),
1270            ArrayArg::from_raw_parts(qh_h, qh.len()),
1271            ArrayArg::from_raw_parts(dd_h, dd.len()),
1272            ArrayArg::from_raw_parts(sc_h, scales.len()),
1273            ArrayArg::from_raw_parts(out_h.clone(), n),
1274            n,
1275        );
1276    }
1277    let bytes = client.read_one_unchecked(out_h);
1278    Ok(f32::from_bytes(&bytes).to_vec())
1279}
1280
1281/// Runs the Q6_K dequant-only kernel (validation/debugging path).
1282pub fn dequantize_q6_k_gpu<R: Runtime>(client: &ComputeClient<R>, data: &[u8]) -> Result<Vec<f32>> {
1283    let (ql, qh, sc, d) = repack_q6_k(data)?;
1284    let n = d.len() * K_SUPERBLOCK;
1285    let ql_h = client.create_from_slice(u32::as_bytes(&ql));
1286    let qh_h = client.create_from_slice(u32::as_bytes(&qh));
1287    let sc_h = client.create_from_slice(u32::as_bytes(&sc));
1288    let d_h = client.create_from_slice(f32::as_bytes(&d));
1289    let out_h = client.empty(n * core::mem::size_of::<f32>());
1290    unsafe {
1291        q6_k_dequant_kernel::launch_unchecked::<R>(
1292            client,
1293            cube_count_1d(n as u32),
1294            CubeDim::new_1d(CUBE_DIM),
1295            ArrayArg::from_raw_parts(ql_h, ql.len()),
1296            ArrayArg::from_raw_parts(qh_h, qh.len()),
1297            ArrayArg::from_raw_parts(sc_h, sc.len()),
1298            ArrayArg::from_raw_parts(d_h, d.len()),
1299            ArrayArg::from_raw_parts(out_h.clone(), n),
1300            n,
1301        );
1302    }
1303    let bytes = client.read_one_unchecked(out_h);
1304    Ok(f32::from_bytes(&bytes).to_vec())
1305}
1306
1307/// A weight matrix resident in VRAM in packed Q4_K form (`[n_out, k]`,
1308/// `k % 256 == 0`, superblocks along `k`).
1309pub struct Q4KWeight<R: Runtime> {
1310    qs: Handle,
1311    dd: Handle,
1312    scales: Handle,
1313    n_out: usize,
1314    k: usize,
1315    _runtime: PhantomData<R>,
1316}
1317
1318impl<R: Runtime> Q4KWeight<R> {
1319    /// Repacks a GGUF Q4_K tensor onto the device.
1320    pub fn from_gguf_bytes(
1321        client: &ComputeClient<R>,
1322        data: &[u8],
1323        n_out: usize,
1324        k: usize,
1325    ) -> Result<Self> {
1326        if k == 0
1327            || k % K_SUPERBLOCK != 0
1328            || data.len() != n_out * k / K_SUPERBLOCK * Q4_K_BLOCK_BYTES
1329        {
1330            return Err(ModelError::BadShape {
1331                tensor: "q4_k weight".into(),
1332                expected: vec![n_out, k],
1333                got: vec![data.len()],
1334            });
1335        }
1336        let (qs, dd, scales) = repack_q4_k(data)?;
1337        Ok(Q4KWeight {
1338            qs: client.create_from_slice(u32::as_bytes(&qs)),
1339            dd: client.create_from_slice(f32::as_bytes(&dd)),
1340            scales: client.create_from_slice(u32::as_bytes(&scales)),
1341            n_out,
1342            k,
1343            _runtime: PhantomData,
1344        })
1345    }
1346
1347    /// Bytes in VRAM: 148 per 256 weights (4.63 bits/weight).
1348    pub fn vram_bytes(&self) -> usize {
1349        (self.n_out * self.k / K_SUPERBLOCK) * (128 + 12 + 8)
1350    }
1351
1352    /// Device path: launch only, output handle returned. Decode (`m == 1`)
1353    /// keeps the untiled kernel; prefill (`m > 1`) takes the shared-memory
1354    /// tiled kernel unless `COMBS_NO_TILED_MATMUL=1`.
1355    pub fn matmul_device(&self, client: &ComputeClient<R>, x: Handle, m: usize) -> Handle {
1356        self.matmul_device_with(client, x, m, m > 1 && tiled_enabled())
1357    }
1358
1359    /// Launch with an explicit kernel choice (the parity tests compare
1360    /// tiled vs untiled on identical inputs).
1361    pub(crate) fn matmul_device_with(
1362        &self,
1363        client: &ComputeClient<R>,
1364        x: Handle,
1365        m: usize,
1366        tiled: bool,
1367    ) -> Handle {
1368        let out_len = m * self.n_out;
1369        let out_h = client.empty(out_len * core::mem::size_of::<f32>());
1370        let n_sb = self.n_out * self.k / K_SUPERBLOCK;
1371        if tiled {
1372            unsafe {
1373                q4_k_matmul_tiled_kernel::launch_unchecked::<R>(
1374                    client,
1375                    cube_count_tiled(self.n_out as u32, m as u32),
1376                    CubeDim::new_1d(CUBE_DIM),
1377                    ArrayArg::from_raw_parts(x, m * self.k),
1378                    ArrayArg::from_raw_parts(self.qs.clone(), n_sb * 32),
1379                    ArrayArg::from_raw_parts(self.dd.clone(), n_sb * 2),
1380                    ArrayArg::from_raw_parts(self.scales.clone(), n_sb * 3),
1381                    ArrayArg::from_raw_parts(out_h.clone(), out_len),
1382                    self.k,
1383                    self.n_out,
1384                );
1385            }
1386        } else {
1387            unsafe {
1388                q4_k_matmul_kernel::launch_unchecked::<R>(
1389                    client,
1390                    cube_count_capped(out_len as u32),
1391                    CubeDim::new_1d(CUBE_DIM),
1392                    ArrayArg::from_raw_parts(x, m * self.k),
1393                    ArrayArg::from_raw_parts(self.qs.clone(), n_sb * 32),
1394                    ArrayArg::from_raw_parts(self.dd.clone(), n_sb * 2),
1395                    ArrayArg::from_raw_parts(self.scales.clone(), n_sb * 3),
1396                    ArrayArg::from_raw_parts(out_h.clone(), out_len),
1397                    m,
1398                    self.k,
1399                    self.n_out,
1400                );
1401            }
1402        }
1403        out_h
1404    }
1405
1406    /// `y = x @ W^T` for host-side `x: [m, k]`, returning `[m, n_out]`.
1407    pub fn matmul_host(&self, client: &ComputeClient<R>, x: &[f32], m: usize) -> Result<Vec<f32>> {
1408        if m == 0 || x.len() != m * self.k {
1409            return Err(ModelError::BadShape {
1410                tensor: "q4_k matmul input".into(),
1411                expected: vec![m, self.k],
1412                got: vec![x.len()],
1413            });
1414        }
1415        let x_h = client.create_from_slice(f32::as_bytes(x));
1416        let out_h = self.matmul_device(client, x_h, m);
1417        let bytes = client.read_one_unchecked(out_h);
1418        Ok(f32::from_bytes(&bytes).to_vec())
1419    }
1420}
1421
1422/// A weight matrix resident in VRAM in packed Q6_K form (`[n_out, k]`,
1423/// `k % 256 == 0`, superblocks along `k`).
1424/// A weight matrix resident in VRAM in packed Q5_K form (`[n_out, k]`,
1425/// `k % 256 == 0`, superblocks along `k`).
1426pub struct Q5KWeight<R: Runtime> {
1427    qs: Handle,
1428    qh: Handle,
1429    dd: Handle,
1430    scales: Handle,
1431    n_out: usize,
1432    k: usize,
1433    _runtime: PhantomData<R>,
1434}
1435
1436impl<R: Runtime> Q5KWeight<R> {
1437    /// Repacks a GGUF Q5_K tensor onto the device.
1438    pub fn from_gguf_bytes(
1439        client: &ComputeClient<R>,
1440        data: &[u8],
1441        n_out: usize,
1442        k: usize,
1443    ) -> Result<Self> {
1444        if k == 0
1445            || k % K_SUPERBLOCK != 0
1446            || data.len() != n_out * k / K_SUPERBLOCK * Q5_K_BLOCK_BYTES
1447        {
1448            return Err(ModelError::BadShape {
1449                tensor: "q5_k weight".into(),
1450                expected: vec![n_out, k],
1451                got: vec![data.len()],
1452            });
1453        }
1454        let (qs, qh, dd, scales) = repack_q5_k(data)?;
1455        Ok(Q5KWeight {
1456            qs: client.create_from_slice(u32::as_bytes(&qs)),
1457            qh: client.create_from_slice(u32::as_bytes(&qh)),
1458            dd: client.create_from_slice(f32::as_bytes(&dd)),
1459            scales: client.create_from_slice(u32::as_bytes(&scales)),
1460            n_out,
1461            k,
1462            _runtime: PhantomData,
1463        })
1464    }
1465
1466    /// Bytes in VRAM: 180 per 256 weights (5.63 bits/weight).
1467    pub fn vram_bytes(&self) -> usize {
1468        (self.n_out * self.k / K_SUPERBLOCK) * (128 + 32 + 12 + 8)
1469    }
1470
1471    /// Device path: launch only, output handle returned (see
1472    /// [`Q40Weight::matmul_device`]).
1473    pub fn matmul_device(&self, client: &ComputeClient<R>, x: Handle, m: usize) -> Handle {
1474        let out_len = m * self.n_out;
1475        let out_h = client.empty(out_len * core::mem::size_of::<f32>());
1476        let n_sb = self.n_out * self.k / K_SUPERBLOCK;
1477        unsafe {
1478            q5_k_matmul_kernel::launch_unchecked::<R>(
1479                client,
1480                cube_count_capped(out_len as u32),
1481                CubeDim::new_1d(CUBE_DIM),
1482                ArrayArg::from_raw_parts(x, m * self.k),
1483                ArrayArg::from_raw_parts(self.qs.clone(), n_sb * 32),
1484                ArrayArg::from_raw_parts(self.qh.clone(), n_sb * 8),
1485                ArrayArg::from_raw_parts(self.dd.clone(), n_sb * 2),
1486                ArrayArg::from_raw_parts(self.scales.clone(), n_sb * 3),
1487                ArrayArg::from_raw_parts(out_h.clone(), out_len),
1488                m,
1489                self.k,
1490                self.n_out,
1491            );
1492        }
1493        out_h
1494    }
1495
1496    /// `y = x @ W^T` for host-side `x: [m, k]`, returning `[m, n_out]`.
1497    pub fn matmul_host(&self, client: &ComputeClient<R>, x: &[f32], m: usize) -> Result<Vec<f32>> {
1498        if m == 0 || x.len() != m * self.k {
1499            return Err(ModelError::BadShape {
1500                tensor: "q5_k matmul input".into(),
1501                expected: vec![m, self.k],
1502                got: vec![x.len()],
1503            });
1504        }
1505        let x_h = client.create_from_slice(f32::as_bytes(x));
1506        let out_h = self.matmul_device(client, x_h, m);
1507        let bytes = client.read_one_unchecked(out_h);
1508        Ok(f32::from_bytes(&bytes).to_vec())
1509    }
1510}
1511
1512pub struct Q6KWeight<R: Runtime> {
1513    ql: Handle,
1514    qh: Handle,
1515    sc: Handle,
1516    d: Handle,
1517    n_out: usize,
1518    k: usize,
1519    _runtime: PhantomData<R>,
1520}
1521
1522impl<R: Runtime> Q6KWeight<R> {
1523    /// Repacks a GGUF Q6_K tensor onto the device.
1524    pub fn from_gguf_bytes(
1525        client: &ComputeClient<R>,
1526        data: &[u8],
1527        n_out: usize,
1528        k: usize,
1529    ) -> Result<Self> {
1530        if k == 0
1531            || k % K_SUPERBLOCK != 0
1532            || data.len() != n_out * k / K_SUPERBLOCK * Q6_K_BLOCK_BYTES
1533        {
1534            return Err(ModelError::BadShape {
1535                tensor: "q6_k weight".into(),
1536                expected: vec![n_out, k],
1537                got: vec![data.len()],
1538            });
1539        }
1540        let (ql, qh, sc, d) = repack_q6_k(data)?;
1541        Ok(Q6KWeight {
1542            ql: client.create_from_slice(u32::as_bytes(&ql)),
1543            qh: client.create_from_slice(u32::as_bytes(&qh)),
1544            sc: client.create_from_slice(u32::as_bytes(&sc)),
1545            d: client.create_from_slice(f32::as_bytes(&d)),
1546            n_out,
1547            k,
1548            _runtime: PhantomData,
1549        })
1550    }
1551
1552    /// Bytes in VRAM: 212 per 256 weights (6.63 bits/weight).
1553    pub fn vram_bytes(&self) -> usize {
1554        (self.n_out * self.k / K_SUPERBLOCK) * (128 + 64 + 16 + 4)
1555    }
1556
1557    /// Device path: launch only, output handle returned (see
1558    /// [`Q40Weight::matmul_device`]).
1559    pub fn matmul_device(&self, client: &ComputeClient<R>, x: Handle, m: usize) -> Handle {
1560        let out_len = m * self.n_out;
1561        let out_h = client.empty(out_len * core::mem::size_of::<f32>());
1562        let n_sb = self.n_out * self.k / K_SUPERBLOCK;
1563        unsafe {
1564            q6_k_matmul_kernel::launch_unchecked::<R>(
1565                client,
1566                cube_count_capped(out_len as u32),
1567                CubeDim::new_1d(CUBE_DIM),
1568                ArrayArg::from_raw_parts(x, m * self.k),
1569                ArrayArg::from_raw_parts(self.ql.clone(), n_sb * 32),
1570                ArrayArg::from_raw_parts(self.qh.clone(), n_sb * 16),
1571                ArrayArg::from_raw_parts(self.sc.clone(), n_sb * 4),
1572                ArrayArg::from_raw_parts(self.d.clone(), n_sb),
1573                ArrayArg::from_raw_parts(out_h.clone(), out_len),
1574                m,
1575                self.k,
1576                self.n_out,
1577            );
1578        }
1579        out_h
1580    }
1581
1582    /// `y = x @ W^T` for host-side `x: [m, k]`, returning `[m, n_out]`.
1583    pub fn matmul_host(&self, client: &ComputeClient<R>, x: &[f32], m: usize) -> Result<Vec<f32>> {
1584        if m == 0 || x.len() != m * self.k {
1585            return Err(ModelError::BadShape {
1586                tensor: "q6_k matmul input".into(),
1587                expected: vec![m, self.k],
1588                got: vec![x.len()],
1589            });
1590        }
1591        let x_h = client.create_from_slice(f32::as_bytes(x));
1592        let out_h = self.matmul_device(client, x_h, m);
1593        let bytes = client.read_one_unchecked(out_h);
1594        Ok(f32::from_bytes(&bytes).to_vec())
1595    }
1596}
1597
1598/// A device-resident quantized weight of any supported format, fixed to the
1599/// engine's wgpu runtime. This is what the linear seam (`qlinear`) stores;
1600/// format dispatch happens once per call, not per element.
1601pub enum QuantWeight {
1602    /// GGUF Q4_0.
1603    Q40(Q40Weight<cubecl::wgpu::WgpuRuntime>),
1604    /// GGUF Q5_0.
1605    Q50(Q50Weight<cubecl::wgpu::WgpuRuntime>),
1606    /// GGUF Q8_0.
1607    Q80(Q80Weight<cubecl::wgpu::WgpuRuntime>),
1608    /// GGUF Q4_K.
1609    Q4K(Q4KWeight<cubecl::wgpu::WgpuRuntime>),
1610    /// GGUF Q5_K.
1611    Q5K(Q5KWeight<cubecl::wgpu::WgpuRuntime>),
1612    /// GGUF Q6_K.
1613    Q6K(Q6KWeight<cubecl::wgpu::WgpuRuntime>),
1614}
1615
1616impl QuantWeight {
1617    /// Builds from a raw packed tensor as handed out by
1618    /// `combs_formats::ModelSource::open_tensor_quant`.
1619    pub fn from_quant_tensor(
1620        client: &ComputeClient<cubecl::wgpu::WgpuRuntime>,
1621        format: combs_formats::QuantFormat,
1622        data: &[u8],
1623        n_out: usize,
1624        k: usize,
1625    ) -> Result<Self> {
1626        use combs_formats::QuantFormat;
1627        Ok(match format {
1628            QuantFormat::Q4_0 => QuantWeight::Q40(Q40Weight::from_gguf_bytes(client, data, n_out, k)?),
1629            QuantFormat::Q5_0 => QuantWeight::Q50(Q50Weight::from_gguf_bytes(client, data, n_out, k)?),
1630            QuantFormat::Q8_0 => QuantWeight::Q80(Q80Weight::from_gguf_bytes(client, data, n_out, k)?),
1631            QuantFormat::Q4K => QuantWeight::Q4K(Q4KWeight::from_gguf_bytes(client, data, n_out, k)?),
1632            QuantFormat::Q5K => QuantWeight::Q5K(Q5KWeight::from_gguf_bytes(client, data, n_out, k)?),
1633            QuantFormat::Q6K => QuantWeight::Q6K(Q6KWeight::from_gguf_bytes(client, data, n_out, k)?),
1634        })
1635    }
1636
1637    /// Output features.
1638    pub fn n_out(&self) -> usize {
1639        match self {
1640            QuantWeight::Q40(w) => w.n_out,
1641            QuantWeight::Q50(w) => w.n_out,
1642            QuantWeight::Q80(w) => w.n_out,
1643            QuantWeight::Q4K(w) => w.n_out,
1644            QuantWeight::Q5K(w) => w.n_out,
1645            QuantWeight::Q6K(w) => w.n_out,
1646        }
1647    }
1648
1649    /// Input features.
1650    pub fn k(&self) -> usize {
1651        match self {
1652            QuantWeight::Q40(w) => w.k,
1653            QuantWeight::Q50(w) => w.k,
1654            QuantWeight::Q80(w) => w.k,
1655            QuantWeight::Q4K(w) => w.k,
1656            QuantWeight::Q5K(w) => w.k,
1657            QuantWeight::Q6K(w) => w.k,
1658        }
1659    }
1660
1661    /// Bytes this weight occupies in VRAM.
1662    pub fn vram_bytes(&self) -> usize {
1663        match self {
1664            QuantWeight::Q40(w) => w.vram_bytes(),
1665            QuantWeight::Q50(w) => w.vram_bytes(),
1666            QuantWeight::Q80(w) => w.vram_bytes(),
1667            QuantWeight::Q4K(w) => w.vram_bytes(),
1668            QuantWeight::Q5K(w) => w.vram_bytes(),
1669            QuantWeight::Q6K(w) => w.vram_bytes(),
1670        }
1671    }
1672
1673    /// Fused dequant-matmul, device handles in and out.
1674    pub fn matmul_device(
1675        &self,
1676        client: &ComputeClient<cubecl::wgpu::WgpuRuntime>,
1677        x: Handle,
1678        m: usize,
1679    ) -> Handle {
1680        match self {
1681            QuantWeight::Q40(w) => w.matmul_device(client, x, m),
1682            QuantWeight::Q50(w) => w.matmul_device(client, x, m),
1683            QuantWeight::Q80(w) => w.matmul_device(client, x, m),
1684            QuantWeight::Q4K(w) => w.matmul_device(client, x, m),
1685            QuantWeight::Q5K(w) => w.matmul_device(client, x, m),
1686            QuantWeight::Q6K(w) => w.matmul_device(client, x, m),
1687        }
1688    }
1689}
1690
1691#[cfg(test)]
1692mod tests {
1693    use super::*;
1694    use cubecl::wgpu::WgpuRuntime;
1695
1696    /// Deterministic pseudo-random Q4_0 block stream: valid finite f16
1697    /// scales, LCG nibble bytes covering the full 0..=255 range.
1698    fn synth_q4_0(n_blocks: usize) -> Vec<u8> {
1699        let mut out = Vec::with_capacity(n_blocks * Q4_0_BLOCK_BYTES);
1700        let mut s = 0x12345678u32;
1701        for b in 0..n_blocks {
1702            let scale = burn::tensor::f16::from_f32(0.003 * ((b % 11) as f32 + 1.0));
1703            out.extend_from_slice(&scale.to_le_bytes());
1704            for _ in 0..16 {
1705                s = s.wrapping_mul(1664525).wrapping_add(1013904223);
1706                out.push((s >> 24) as u8);
1707            }
1708        }
1709        out
1710    }
1711
1712    /// Plain f32 reference matmul over the CPU-dequantized weight.
1713    fn ref_matmul(x: &[f32], w: &[f32], m: usize, k: usize, n_out: usize) -> Vec<f32> {
1714        let mut out = vec![0f32; m * n_out];
1715        for r in 0..m {
1716            for c in 0..n_out {
1717                let mut acc = 0f32;
1718                for i in 0..k {
1719                    acc += x[r * k + i] * w[c * k + i];
1720                }
1721                out[r * n_out + c] = acc;
1722            }
1723        }
1724        out
1725    }
1726
1727    /// The GPU dequant must be **bit-exact** with the harmony CPU reference
1728    /// (`combs_formats::quants::dequantize_q4_0`) — same unpack, same
1729    /// arithmetic, same f16→f32 scale conversion. This validates the whole
1730    /// Layout layer: any repack/indexing slip shows up as a hard mismatch.
1731    #[test]
1732    fn dequant_kernel_is_bit_exact_vs_cpu_reference() {
1733        if crate::skip_no_gpu() {
1734            return;
1735        }
1736        let n_blocks = 33; // deliberately not a multiple of the cube dim
1737        let data = synth_q4_0(n_blocks);
1738        let n = n_blocks * Q4_0_BLOCK;
1739        let expect = combs_formats::quants::dequantize_q4_0(&data, n).unwrap();
1740
1741        let device = Default::default();
1742        let client = WgpuRuntime::client(&device);
1743        let got = dequantize_q4_0_gpu::<WgpuRuntime>(&client, &data).unwrap();
1744
1745        assert_eq!(got, expect, "GPU dequant must be bit-exact vs gguf.rs");
1746    }
1747
1748    /// The fused kernel must match a reference matmul over the reference
1749    /// dequant within accumulation-order tolerance, for both the decode
1750    /// shape (m=1) and a prefill shape (m>1), across multiple cubes.
1751    #[test]
1752    fn fused_matmul_matches_reference() {
1753        if crate::skip_no_gpu() {
1754            return;
1755        }
1756        let (n_out, k) = (67, 128); // 67 forces a partial second cube
1757        let n_blocks = n_out * k / Q4_0_BLOCK;
1758        let data = synth_q4_0(n_blocks);
1759        let w = combs_formats::quants::dequantize_q4_0(&data, n_out * k).unwrap();
1760
1761        let device = Default::default();
1762        let client = WgpuRuntime::client(&device);
1763        let weight = Q40Weight::<WgpuRuntime>::from_gguf_bytes(&client, &data, n_out, k).unwrap();
1764        assert_eq!(weight.vram_bytes(), n_blocks * 20);
1765
1766        for m in [1usize, 3] {
1767            let x: Vec<f32> = (0..m * k)
1768                .map(|i| ((i * 7 % 13) as f32 - 6.0) / 8.0)
1769                .collect();
1770            let expect = ref_matmul(&x, &w, m, k, n_out);
1771            let got = weight.matmul_host(&client, &x, m).unwrap();
1772            assert_eq!(got.len(), expect.len());
1773            for (i, (g, e)) in got.iter().zip(expect.iter()).enumerate() {
1774                let tol = 1e-4 * e.abs().max(1.0);
1775                assert!(
1776                    (g - e).abs() <= tol,
1777                    "m={m} out[{i}]: got {g}, expect {e}"
1778                );
1779            }
1780        }
1781    }
1782
1783    /// Deterministic pseudo-random byte stream for K-quant payloads.
1784    fn lcg_bytes(n: usize, seed: u32) -> Vec<u8> {
1785        let mut s = seed;
1786        (0..n)
1787            .map(|_| {
1788                s = s.wrapping_mul(1664525).wrapping_add(1013904223);
1789                (s >> 24) as u8
1790            })
1791            .collect()
1792    }
1793
1794    /// Q4_K superblock stream: valid small f16 d/dmin, LCG scales + quants.
1795    fn synth_q4_k(n_sb: usize) -> Vec<u8> {
1796        let mut out = Vec::with_capacity(n_sb * Q4_K_BLOCK_BYTES);
1797        for b in 0..n_sb {
1798            let d = burn::tensor::f16::from_f32(0.002 * ((b % 9) as f32 + 1.0));
1799            let dmin = burn::tensor::f16::from_f32(0.001 * ((b % 5) as f32 + 1.0));
1800            out.extend_from_slice(&d.to_le_bytes());
1801            out.extend_from_slice(&dmin.to_le_bytes());
1802            out.extend_from_slice(&lcg_bytes(140, 0xC0FFEE ^ b as u32));
1803        }
1804        out
1805    }
1806
1807    /// Q5_K superblock stream: LCG scales/qh/qs, valid small f16 d/dmin.
1808    fn synth_q5_k(n_sb: usize) -> Vec<u8> {
1809        let mut out = Vec::with_capacity(n_sb * Q5_K_BLOCK_BYTES);
1810        for b in 0..n_sb {
1811            let d = burn::tensor::f16::from_f32(0.003 * ((b % 7) as f32 + 1.0));
1812            let dmin = burn::tensor::f16::from_f32(0.001 * ((b % 5) as f32 + 1.0));
1813            out.extend_from_slice(&d.to_le_bytes());
1814            out.extend_from_slice(&dmin.to_le_bytes());
1815            out.extend_from_slice(&lcg_bytes(172, 0x5EED ^ b as u32));
1816        }
1817        out
1818    }
1819
1820    /// Q6_K superblock stream: LCG ql/qh/scales, valid small f16 d.
1821    fn synth_q6_k(n_sb: usize) -> Vec<u8> {
1822        let mut out = Vec::with_capacity(n_sb * Q6_K_BLOCK_BYTES);
1823        for b in 0..n_sb {
1824            out.extend_from_slice(&lcg_bytes(208, 0xBEE5 ^ b as u32));
1825            let d = burn::tensor::f16::from_f32(0.002 * ((b % 9) as f32 + 1.0));
1826            out.extend_from_slice(&d.to_le_bytes());
1827        }
1828        out
1829    }
1830
1831    fn assert_close(got: &[f32], expect: &[f32], rel: f32, what: &str) {
1832        assert_eq!(got.len(), expect.len(), "{what}: length");
1833        for (i, (g, e)) in got.iter().zip(expect.iter()).enumerate() {
1834            let tol = rel * e.abs().max(1.0);
1835            assert!((g - e).abs() <= tol, "{what}[{i}]: got {g}, expect {e}");
1836        }
1837    }
1838
1839    /// Q4_K GPU dequant vs the harmony CPU reference. The kernel mirrors the
1840    /// reference arithmetic exactly; tolerance only allows for backend FMA
1841    /// contraction of `d1·q − fmin` (a last-ulp effect, bounded far below
1842    /// the quantization step).
1843    #[test]
1844    fn q4_k_dequant_matches_cpu_reference() {
1845        if crate::skip_no_gpu() {
1846            return;
1847        }
1848        let n_sb = 9;
1849        let data = synth_q4_k(n_sb);
1850        let n = n_sb * K_SUPERBLOCK;
1851        let expect = combs_formats::quants::dequantize_q4_k(&data, n).unwrap();
1852
1853        let device = Default::default();
1854        let client = WgpuRuntime::client(&device);
1855        let got = dequantize_q4_k_gpu::<WgpuRuntime>(&client, &data).unwrap();
1856        assert_close(&got, &expect, 1e-6, "q4_k dequant");
1857    }
1858
1859    /// Q5_K GPU dequant vs the harmony CPU reference.
1860    #[test]
1861    fn q5_k_dequant_matches_cpu_reference() {
1862        if crate::skip_no_gpu() {
1863            return;
1864        }
1865        let n_sb = 9;
1866        let data = synth_q5_k(n_sb);
1867        let n = n_sb * K_SUPERBLOCK;
1868        let expect = combs_formats::quants::dequantize_q5_k(&data, n).unwrap();
1869
1870        let device = Default::default();
1871        let client = WgpuRuntime::client(&device);
1872        let got = dequantize_q5_k_gpu::<WgpuRuntime>(&client, &data).unwrap();
1873        assert_close(&got, &expect, 1e-6, "q5_k dequant");
1874    }
1875
1876    /// Q6_K GPU dequant vs the harmony CPU reference.
1877    #[test]
1878    fn q6_k_dequant_matches_cpu_reference() {
1879        if crate::skip_no_gpu() {
1880            return;
1881        }
1882        let n_sb = 9;
1883        let data = synth_q6_k(n_sb);
1884        let n = n_sb * K_SUPERBLOCK;
1885        let expect = combs_formats::quants::dequantize_q6_k(&data, n).unwrap();
1886
1887        let device = Default::default();
1888        let client = WgpuRuntime::client(&device);
1889        let got = dequantize_q6_k_gpu::<WgpuRuntime>(&client, &data).unwrap();
1890        assert_close(&got, &expect, 1e-6, "q6_k dequant");
1891    }
1892
1893    /// Fused Q5_K matmul vs a reference matmul over the reference dequant,
1894    /// decode (m=1) and prefill (m>1) shapes, multi-superblock rows.
1895    #[test]
1896    fn q5_k_fused_matmul_matches_reference() {
1897        if crate::skip_no_gpu() {
1898            return;
1899        }
1900        let (n_out, k) = (35, 512); // 2 superblocks per row, partial cube
1901        let n_sb = n_out * k / K_SUPERBLOCK;
1902        let data = synth_q5_k(n_sb);
1903        let w = combs_formats::quants::dequantize_q5_k(&data, n_out * k).unwrap();
1904
1905        let device = Default::default();
1906        let client = WgpuRuntime::client(&device);
1907        let weight = Q5KWeight::<WgpuRuntime>::from_gguf_bytes(&client, &data, n_out, k).unwrap();
1908        assert_eq!(weight.vram_bytes(), n_sb * 180);
1909
1910        for m in [1usize, 3] {
1911            let x: Vec<f32> = (0..m * k)
1912                .map(|i| ((i * 7 % 13) as f32 - 6.0) / 8.0)
1913                .collect();
1914            let expect = ref_matmul(&x, &w, m, k, n_out);
1915            let got = weight.matmul_host(&client, &x, m).unwrap();
1916            assert_close(&got, &expect, 1e-3, &format!("q5_k matmul m={m}"));
1917        }
1918    }
1919
1920    /// Fused Q4_K matmul vs a reference matmul over the reference dequant,
1921    /// decode (m=1) and prefill (m>1) shapes, multi-superblock rows.
1922    #[test]
1923    fn q4_k_fused_matmul_matches_reference() {
1924        if crate::skip_no_gpu() {
1925            return;
1926        }
1927        let (n_out, k) = (35, 512); // 2 superblocks per row, partial cube
1928        let n_sb = n_out * k / K_SUPERBLOCK;
1929        let data = synth_q4_k(n_sb);
1930        let w = combs_formats::quants::dequantize_q4_k(&data, n_out * k).unwrap();
1931
1932        let device = Default::default();
1933        let client = WgpuRuntime::client(&device);
1934        let weight = Q4KWeight::<WgpuRuntime>::from_gguf_bytes(&client, &data, n_out, k).unwrap();
1935        assert_eq!(weight.vram_bytes(), n_sb * 148);
1936
1937        for m in [1usize, 3] {
1938            let x: Vec<f32> = (0..m * k)
1939                .map(|i| ((i * 7 % 13) as f32 - 6.0) / 8.0)
1940                .collect();
1941            let expect = ref_matmul(&x, &w, m, k, n_out);
1942            let got = weight.matmul_host(&client, &x, m).unwrap();
1943            assert_close(&got, &expect, 1e-3, &format!("q4_k matmul m={m}"));
1944        }
1945    }
1946
1947    /// Fused Q6_K matmul vs a reference matmul over the reference dequant.
1948    #[test]
1949    fn q6_k_fused_matmul_matches_reference() {
1950        if crate::skip_no_gpu() {
1951            return;
1952        }
1953        let (n_out, k) = (35, 512);
1954        let n_sb = n_out * k / K_SUPERBLOCK;
1955        let data = synth_q6_k(n_sb);
1956        let w = combs_formats::quants::dequantize_q6_k(&data, n_out * k).unwrap();
1957
1958        let device = Default::default();
1959        let client = WgpuRuntime::client(&device);
1960        let weight = Q6KWeight::<WgpuRuntime>::from_gguf_bytes(&client, &data, n_out, k).unwrap();
1961        assert_eq!(weight.vram_bytes(), n_sb * 212);
1962
1963        for m in [1usize, 3] {
1964            let x: Vec<f32> = (0..m * k)
1965                .map(|i| ((i * 7 % 13) as f32 - 6.0) / 8.0)
1966                .collect();
1967            let expect = ref_matmul(&x, &w, m, k, n_out);
1968            let got = weight.matmul_host(&client, &x, m).unwrap();
1969            assert_close(&got, &expect, 1e-3, &format!("q6_k matmul m={m}"));
1970        }
1971    }
1972
1973    /// Q5_0 stream: valid f16 scales, LCG high bits + nibbles.
1974    fn synth_q5_0(n_blocks: usize) -> Vec<u8> {
1975        let mut out = Vec::with_capacity(n_blocks * Q5_0_BLOCK_BYTES);
1976        for b in 0..n_blocks {
1977            let scale = burn::tensor::f16::from_f32(0.003 * ((b % 11) as f32 + 1.0));
1978            out.extend_from_slice(&scale.to_le_bytes());
1979            out.extend_from_slice(&lcg_bytes(20, 0x51D0 ^ b as u32));
1980        }
1981        out
1982    }
1983
1984    /// Q8_0 stream: valid f16 scales, LCG i8 payload (full range).
1985    fn synth_q8_0(n_blocks: usize) -> Vec<u8> {
1986        let mut out = Vec::with_capacity(n_blocks * Q8_0_BLOCK_BYTES);
1987        for b in 0..n_blocks {
1988            let scale = burn::tensor::f16::from_f32(0.003 * ((b % 11) as f32 + 1.0));
1989            out.extend_from_slice(&scale.to_le_bytes());
1990            out.extend_from_slice(&lcg_bytes(32, 0x80C0 ^ b as u32));
1991        }
1992        out
1993    }
1994
1995    /// Q5_0/Q8_0 GPU dequant must be **bit-exact** vs the CPU references —
1996    /// both are a single f32 multiply per value, same as Q4_0.
1997    #[test]
1998    fn q5_0_and_q8_0_dequant_are_bit_exact() {
1999        if crate::skip_no_gpu() {
2000            return;
2001        }
2002        let device = Default::default();
2003        let client = WgpuRuntime::client(&device);
2004
2005        let data = synth_q5_0(33);
2006        let n = 33 * Q4_0_BLOCK;
2007        let expect = combs_formats::quants::dequantize_q5_0(&data, n).unwrap();
2008        let got = dequantize_q5_0_gpu::<WgpuRuntime>(&client, &data).unwrap();
2009        assert_eq!(got, expect, "q5_0 GPU dequant must be bit-exact");
2010
2011        let data = synth_q8_0(33);
2012        let expect = combs_formats::quants::dequantize_q8_0(&data, n).unwrap();
2013        let got = dequantize_q8_0_gpu::<WgpuRuntime>(&client, &data).unwrap();
2014        assert_eq!(got, expect, "q8_0 GPU dequant must be bit-exact");
2015    }
2016
2017    /// Fused Q5_0/Q8_0 matmuls vs reference matmuls over the reference
2018    /// dequants, decode and prefill shapes.
2019    #[test]
2020    fn q5_0_and_q8_0_fused_matmul_match_reference() {
2021        if crate::skip_no_gpu() {
2022            return;
2023        }
2024        let device = Default::default();
2025        let client = WgpuRuntime::client(&device);
2026        let (n_out, k) = (67, 128);
2027        let n_blocks = n_out * k / Q4_0_BLOCK;
2028
2029        let data5 = synth_q5_0(n_blocks);
2030        let w5 = combs_formats::quants::dequantize_q5_0(&data5, n_out * k).unwrap();
2031        let q5 = Q50Weight::<WgpuRuntime>::from_gguf_bytes(&client, &data5, n_out, k).unwrap();
2032        assert_eq!(q5.vram_bytes(), n_blocks * 24);
2033
2034        let data8 = synth_q8_0(n_blocks);
2035        let w8 = combs_formats::quants::dequantize_q8_0(&data8, n_out * k).unwrap();
2036        let q8 = Q80Weight::<WgpuRuntime>::from_gguf_bytes(&client, &data8, n_out, k).unwrap();
2037        assert_eq!(q8.vram_bytes(), n_blocks * 36);
2038
2039        for m in [1usize, 3] {
2040            let x: Vec<f32> = (0..m * k)
2041                .map(|i| ((i * 7 % 13) as f32 - 6.0) / 8.0)
2042                .collect();
2043            let got5 = q5.matmul_host(&client, &x, m).unwrap();
2044            assert_close(&got5, &ref_matmul(&x, &w5, m, k, n_out), 1e-3, &format!("q5_0 m={m}"));
2045            let got8 = q8.matmul_host(&client, &x, m).unwrap();
2046            assert_close(&got8, &ref_matmul(&x, &w8, m, k, n_out), 1e-3, &format!("q8_0 m={m}"));
2047        }
2048    }
2049
2050    /// Q5_0/Q8_0 malformed input rejection.
2051    #[test]
2052    fn q5_q8_shape_validation() {
2053        if crate::skip_no_gpu() {
2054            return;
2055        }
2056        let device = Default::default();
2057        let client = WgpuRuntime::client(&device);
2058        assert!(repack_q5_0(&[0u8; 21]).is_err());
2059        assert!(repack_q8_0(&[0u8; 33]).is_err());
2060        assert!(Q50Weight::<WgpuRuntime>::from_gguf_bytes(&client, &synth_q5_0(2), 2, 31).is_err());
2061        assert!(Q80Weight::<WgpuRuntime>::from_gguf_bytes(&client, &synth_q8_0(2), 2, 64).is_err());
2062    }
2063
2064    /// K-quant shape validation mirrors the Q4_0 rules.
2065    #[test]
2066    fn k_quant_shape_validation() {
2067        if crate::skip_no_gpu() {
2068            return;
2069        }
2070        let device = Default::default();
2071        let client = WgpuRuntime::client(&device);
2072        assert!(repack_q4_k(&[0u8; 143]).is_err());
2073        assert!(repack_q6_k(&[0u8; 209]).is_err());
2074        // k must be a superblock multiple.
2075        assert!(
2076            Q4KWeight::<WgpuRuntime>::from_gguf_bytes(&client, &synth_q4_k(1), 1, 128).is_err()
2077        );
2078        assert!(
2079            Q6KWeight::<WgpuRuntime>::from_gguf_bytes(&client, &synth_q6_k(1), 1, 128).is_err()
2080        );
2081    }
2082
2083    /// Compares the tiled and untiled kernels on identical device inputs
2084    /// and demands **bit-identical** outputs (`to_bits`, not a tolerance):
2085    /// the tiled kernels change only the activation load path, never the
2086    /// accumulation order.
2087    fn assert_tiled_bit_identical(
2088        untiled: &[f32],
2089        tiled: &[f32],
2090        label: &str,
2091    ) {
2092        assert_eq!(untiled.len(), tiled.len(), "{label}: length mismatch");
2093        for (i, (u, t)) in untiled.iter().zip(tiled.iter()).enumerate() {
2094            assert_eq!(
2095                u.to_bits(),
2096                t.to_bits(),
2097                "{label} out[{i}]: untiled {u} vs tiled {t} — accumulation order drifted"
2098            );
2099        }
2100    }
2101
2102    /// Q8_0 tiled-vs-untiled bit identity across ragged shapes: n_out not a
2103    /// multiple of the cube dim (ragged final column block), k not a
2104    /// multiple of the tile (ragged final k-tile), m from tiny to a full
2105    /// prefill chunk.
2106    #[test]
2107    fn tiled_q8_0_matmul_is_bit_identical() {
2108        if crate::skip_no_gpu() {
2109            return;
2110        }
2111        let (n_out, k) = (300, 320);
2112        let n_blocks = n_out * k / Q4_0_BLOCK;
2113        let data = synth_q8_0(n_blocks);
2114        let device = Default::default();
2115        let client = WgpuRuntime::client(&device);
2116        let w = Q80Weight::<WgpuRuntime>::from_gguf_bytes(&client, &data, n_out, k).unwrap();
2117        for m in [2usize, 3, 17, 256, 1024] {
2118            let x: Vec<f32> = (0..m * k)
2119                .map(|i| ((i * 11 % 29) as f32 - 14.0) / 16.0)
2120                .collect();
2121            let x_h = client.create_from_slice(f32::as_bytes(&x));
2122            let un_h = w.matmul_device_with(&client, x_h.clone(), m, false);
2123            let ti_h = w.matmul_device_with(&client, x_h, m, true);
2124            let un = f32::from_bytes(&client.read_one_unchecked(un_h)).to_vec();
2125            let ti = f32::from_bytes(&client.read_one_unchecked(ti_h)).to_vec();
2126            assert_tiled_bit_identical(&un, &ti, &format!("q8_0 m={m}"));
2127        }
2128    }
2129
2130    /// Q4_K tiled-vs-untiled bit identity (superblock-aligned k by
2131    /// construction; ragged final column block still exercised).
2132    #[test]
2133    fn tiled_q4_k_matmul_is_bit_identical() {
2134        if crate::skip_no_gpu() {
2135            return;
2136        }
2137        let (n_out, k) = (300, 512);
2138        let n_sb = n_out * k / K_SUPERBLOCK;
2139        let data = synth_q4_k(n_sb);
2140        let device = Default::default();
2141        let client = WgpuRuntime::client(&device);
2142        let w = Q4KWeight::<WgpuRuntime>::from_gguf_bytes(&client, &data, n_out, k).unwrap();
2143        for m in [2usize, 3, 17, 256, 1024] {
2144            let x: Vec<f32> = (0..m * k)
2145                .map(|i| ((i * 13 % 31) as f32 - 15.0) / 16.0)
2146                .collect();
2147            let x_h = client.create_from_slice(f32::as_bytes(&x));
2148            let un_h = w.matmul_device_with(&client, x_h.clone(), m, false);
2149            let ti_h = w.matmul_device_with(&client, x_h, m, true);
2150            let un = f32::from_bytes(&client.read_one_unchecked(un_h)).to_vec();
2151            let ti = f32::from_bytes(&client.read_one_unchecked(ti_h)).to_vec();
2152            assert_tiled_bit_identical(&un, &ti, &format!("q4_k m={m}"));
2153        }
2154    }
2155
2156    /// Malformed inputs must be rejected, not mis-indexed.
2157    #[test]
2158    fn shape_validation() {
2159        if crate::skip_no_gpu() {
2160            return;
2161        }
2162        let device = Default::default();
2163        let client = WgpuRuntime::client(&device);
2164        // Truncated block stream.
2165        assert!(repack_q4_0(&[0u8; 17]).is_err());
2166        // k not a multiple of the block size.
2167        assert!(Q40Weight::<WgpuRuntime>::from_gguf_bytes(&client, &synth_q4_0(2), 2, 31).is_err());
2168        // Byte count disagrees with [n_out, k].
2169        assert!(Q40Weight::<WgpuRuntime>::from_gguf_bytes(&client, &synth_q4_0(2), 2, 64).is_err());
2170        // Bad x length.
2171        let w = Q40Weight::<WgpuRuntime>::from_gguf_bytes(&client, &synth_q4_0(2), 2, 32).unwrap();
2172        assert!(w.matmul_host(&client, &[0f32; 31], 1).is_err());
2173    }
2174}