Skip to main content

hanzo_ml/quantized/
mod.rs

1use crate::{
2    backend::BackendStorage, CpuStorage, DType, Device, Result, Shape, Storage, Tensor, D,
3};
4use iq_quants::*;
5use k_quants::*;
6use std::borrow::Cow;
7
8#[cfg(target_feature = "avx2")]
9pub mod avx;
10mod dummy_cuda;
11mod dummy_metal;
12pub mod ggml_file;
13pub mod gguf_file;
14mod iq_grids;
15pub mod iq_quants;
16pub mod imatrix_file;
17pub mod k_quants;
18#[cfg(feature = "metal")]
19pub mod metal;
20#[cfg(not(target_arch = "wasm32"))]
21pub mod tokenizer;
22#[cfg(not(feature = "metal"))]
23mod metal {
24    pub use super::dummy_metal::*;
25}
26#[cfg(feature = "cuda")]
27pub mod cuda;
28#[cfg(feature = "cuda")]
29pub mod fast_mmq;
30#[cfg(feature = "cuda")]
31pub mod fast_mmvq;
32#[cfg(not(feature = "cuda"))]
33mod cuda {
34    pub use super::dummy_cuda::*;
35}
36
37#[cfg(target_feature = "neon")]
38pub mod neon;
39#[cfg(target_feature = "simd128")]
40pub mod simd128;
41pub mod utils;
42// Declarative GGUF quant-type formatter (Cut 2). Additive: defines `quant_format!` and a
43// `#[cfg(test)]` equivalence proof; does not alter any existing type or wiring.
44pub mod quant_format;
45use half::{bf16, f16};
46
47pub use k_quants::GgmlType;
48
49// Borrows `data` (does not consume it) so the returned slice stays valid for the caller's lifetime.
50// Taking `Cow` by value here was a use-after-free: the Cow dropped at return, dangling the slice for
51// Cow::Owned inputs (segfault on large quantized tensors; mmap'd Cow::Borrowed happened to survive).
52fn as_t_slice<T>(data: &[u8]) -> &[T] {
53    let size = std::mem::size_of::<T>();
54    assert_eq!(
55        data.len() % size,
56        0,
57        "Data length must be a multiple of T's size"
58    );
59    let ptr = data.as_ptr();
60    assert_eq!(
61        (ptr as usize) % std::mem::align_of::<T>(),
62        0,
63        "Data pointer must be aligned to T's alignment"
64    );
65    unsafe { std::slice::from_raw_parts(ptr as *const T, data.len() / size) }
66}
67
68pub struct QTensor {
69    storage: QStorage,
70    shape: Shape,
71}
72
73impl Device {
74    fn qzeros(&self, elem_count: usize, dtype: GgmlDType) -> Result<QStorage> {
75        match self {
76            Device::Cpu => {
77                let storage = dtype.cpu_zeros(elem_count);
78                Ok(QStorage::Cpu(storage))
79            }
80            Device::Metal(metal) => {
81                let storage = metal::QMetalStorage::zeros(metal, elem_count, dtype)?;
82                Ok(QStorage::Metal(storage))
83            }
84            Device::Cuda(cuda) => {
85                let storage = cuda::QCudaStorage::zeros(cuda, elem_count, dtype)?;
86                Ok(QStorage::Cuda(storage))
87            }
88            #[cfg(feature = "rocm")]
89            Device::Rocm(d) => {
90                // Mirror Vulkan: keep quantized blocks in a CPU box + the device; QMatMul
91                // dequantizes them to a dense ROCm tensor on use.
92                let storage = dtype.cpu_zeros(elem_count);
93                Ok(QStorage::Rocm(storage, d.clone()))
94            }
95            #[cfg(feature = "vulkan")]
96            Device::Vulkan(d) => {
97                // Keep quantized blocks in a CPU box + the device (same as from_data); QMatMul's
98                // VulkanQuant path uploads/dequantizes them to the GPU on use.
99                let storage = dtype.cpu_zeros(elem_count);
100                Ok(QStorage::Vulkan(storage, d.clone()))
101            }
102            #[cfg(feature = "wgpu")]
103            Device::Wgpu(d) => {
104                // Same as Vulkan: keep the quantized blocks in a CPU box + the device; QMatMul's
105                // WgpuQuant path uploads them to the GPU on use.
106                let storage = dtype.cpu_zeros(elem_count);
107                Ok(QStorage::Wgpu(storage, d.clone()))
108            }
109        }
110    }
111}
112
113pub enum QStorage {
114    Cpu(Box<dyn QuantizedType>),
115    Metal(metal::QMetalStorage),
116    Cuda(cuda::QCudaStorage),
117    // ROCm mirrors the Vulkan path: quantized blocks held in a CPU box + the device; QMatMul
118    // dequantizes them to a dense f32 ROCm tensor on demand (rocBLAS matmul). A native HIP quant
119    // matmul is the bandwidth-win follow-up.
120    #[cfg(feature = "rocm")]
121    Rocm(Box<dyn QuantizedType>, crate::RocmDevice),
122    // Vulkan keeps the quantized blocks in a CPU box and dequantizes to an f32 Vulkan tensor on
123    // demand (QMatMul forces dequantize for Vulkan). Lets GGUF-quantized models run on the GPU;
124    // a direct quantized matmul (the Q8 kernel) is the bandwidth-win follow-up.
125    #[cfg(feature = "vulkan")]
126    Vulkan(Box<dyn QuantizedType>, crate::VulkanDevice),
127    // wgpu mirror of the Vulkan path: quantized blocks held in a CPU box + the device. QMatMul's
128    // WgpuQuant path reads the GGML bytes straight in the native quant matvec kernel (decode), or
129    // dequantizes to an f32 wgpu tensor on demand.
130    #[cfg(feature = "wgpu")]
131    Wgpu(Box<dyn QuantizedType>, crate::WgpuDevice),
132}
133
134impl QStorage {
135    pub fn from_data(data: Cow<'_, [u8]>, device: &Device, dtype: GgmlDType) -> Result<Self> {
136        match device {
137            Device::Cpu => Ok(Self::Cpu(dtype.from_data(data))),
138            Device::Metal(d) => match dtype {
139                GgmlDType::F32 => metal::load_quantized(d, as_t_slice::<f32>(&data)),
140                GgmlDType::F16 => metal::load_quantized(d, as_t_slice::<f16>(&data)),
141                GgmlDType::Q4_0 => metal::load_quantized(d, as_t_slice::<BlockQ4_0>(&data)),
142                GgmlDType::Q4_1 => metal::load_quantized(d, as_t_slice::<BlockQ4_1>(&data)),
143                GgmlDType::Q5_0 => metal::load_quantized(d, as_t_slice::<BlockQ5_0>(&data)),
144                GgmlDType::Q5_1 => metal::load_quantized(d, as_t_slice::<BlockQ5_1>(&data)),
145                GgmlDType::Q8_0 => metal::load_quantized(d, as_t_slice::<BlockQ8_0>(&data)),
146                GgmlDType::Q8_1 => metal::load_quantized(d, as_t_slice::<BlockQ8_1>(&data)),
147                GgmlDType::Q2K => metal::load_quantized(d, as_t_slice::<BlockQ2K>(&data)),
148                GgmlDType::Q3K => metal::load_quantized(d, as_t_slice::<BlockQ3K>(&data)),
149                GgmlDType::Q4K => metal::load_quantized(d, as_t_slice::<BlockQ4K>(&data)),
150                GgmlDType::Q5K => metal::load_quantized(d, as_t_slice::<BlockQ5K>(&data)),
151                GgmlDType::Q6K => metal::load_quantized(d, as_t_slice::<BlockQ6K>(&data)),
152                GgmlDType::Q8K => metal::load_quantized(d, as_t_slice::<BlockQ8K>(&data)),
153                GgmlDType::IQ4_NL => metal::load_quantized(d, as_t_slice::<BlockIQ4nl>(&data)),
154                GgmlDType::IQ4_XS => metal::load_quantized(d, as_t_slice::<BlockIQ4xs>(&data)),
155                GgmlDType::MXFP4 => metal::load_quantized(d, as_t_slice::<BlockMXFP4>(&data)),
156                GgmlDType::BF16 => metal::load_quantized(d, as_t_slice::<bf16>(&data)),
157                // IQ / ternary / 1-bit / NVFP4 codec types have no native Metal loader (CPU-decode only).
158                other => crate::bail!("{other:?} is not supported on the Metal backend"),
159            },
160            Device::Cuda(d) => match dtype {
161                GgmlDType::F32 => cuda::load_quantized(d, as_t_slice::<f32>(&data)),
162                GgmlDType::F16 => cuda::load_quantized(d, as_t_slice::<f16>(&data)),
163                GgmlDType::Q4_0 => cuda::load_quantized(d, as_t_slice::<BlockQ4_0>(&data)),
164                GgmlDType::Q4_1 => cuda::load_quantized(d, as_t_slice::<BlockQ4_1>(&data)),
165                GgmlDType::Q5_0 => cuda::load_quantized(d, as_t_slice::<BlockQ5_0>(&data)),
166                GgmlDType::Q5_1 => cuda::load_quantized(d, as_t_slice::<BlockQ5_1>(&data)),
167                GgmlDType::Q8_0 => cuda::load_quantized(d, as_t_slice::<BlockQ8_0>(&data)),
168                GgmlDType::Q8_1 => cuda::load_quantized(d, as_t_slice::<BlockQ8_1>(&data)),
169                GgmlDType::Q2K => cuda::load_quantized(d, as_t_slice::<BlockQ2K>(&data)),
170                GgmlDType::Q3K => cuda::load_quantized(d, as_t_slice::<BlockQ3K>(&data)),
171                GgmlDType::Q4K => cuda::load_quantized(d, as_t_slice::<BlockQ4K>(&data)),
172                GgmlDType::Q5K => cuda::load_quantized(d, as_t_slice::<BlockQ5K>(&data)),
173                GgmlDType::Q6K => cuda::load_quantized(d, as_t_slice::<BlockQ6K>(&data)),
174                GgmlDType::Q8K => cuda::load_quantized(d, as_t_slice::<BlockQ8K>(&data)),
175                GgmlDType::IQ4_NL => cuda::load_quantized(d, as_t_slice::<BlockIQ4nl>(&data)),
176                GgmlDType::IQ4_XS => cuda::load_quantized(d, as_t_slice::<BlockIQ4xs>(&data)),
177                GgmlDType::MXFP4 => cuda::load_quantized(d, as_t_slice::<BlockMXFP4>(&data)),
178                GgmlDType::BF16 => cuda::load_quantized(d, as_t_slice::<bf16>(&data)),
179                // IQ / ternary / 1-bit / NVFP4 codec types: no native on-GPU quant-matmul kernel, but the
180                // GGML blocks upload to VRAM byte-for-byte like any other quant. QCudaStorage::fwd then
181                // dequantizes them to f32 (CPU codebook decode + upload) for a dense matmul -- see
182                // `has_native_q8_1_matmul`. So an i-quant GGUF that used to bail here now LOADS and DECODES
183                // on CUDA, exact w.r.t. the CPU reference; a native mmvq kernel is the bandwidth follow-up.
184                // Exhaustive on purpose: a future GgmlDType must be wired here (fail-closed at compile) rather
185                // than silently bailing at load.
186                GgmlDType::IQ2_XXS => cuda::load_quantized(d, as_t_slice::<BlockIQ2xxs>(&data)),
187                GgmlDType::IQ2_XS => cuda::load_quantized(d, as_t_slice::<BlockIQ2xs>(&data)),
188                GgmlDType::IQ2_S => cuda::load_quantized(d, as_t_slice::<BlockIQ2s>(&data)),
189                GgmlDType::IQ3_XXS => cuda::load_quantized(d, as_t_slice::<BlockIQ3xxs>(&data)),
190                GgmlDType::IQ3_S => cuda::load_quantized(d, as_t_slice::<BlockIQ3s>(&data)),
191                GgmlDType::IQ1_S => cuda::load_quantized(d, as_t_slice::<BlockIQ1s>(&data)),
192                GgmlDType::IQ1_M => cuda::load_quantized(d, as_t_slice::<BlockIQ1m>(&data)),
193                GgmlDType::TQ1_0 => cuda::load_quantized(d, as_t_slice::<BlockTQ1_0>(&data)),
194                GgmlDType::TQ2_0 => cuda::load_quantized(d, as_t_slice::<BlockTQ2_0>(&data)),
195                GgmlDType::NVFP4 => cuda::load_quantized(d, as_t_slice::<BlockNVFP4>(&data)),
196                GgmlDType::Q1_0 => cuda::load_quantized(d, as_t_slice::<BlockQ1_0>(&data)),
197            },
198            #[cfg(feature = "rocm")]
199            Device::Rocm(d) => Ok(Self::Rocm(dtype.from_data(data), d.clone())),
200            #[cfg(feature = "vulkan")]
201            Device::Vulkan(d) => Ok(Self::Vulkan(dtype.from_data(data), d.clone())),
202            #[cfg(feature = "wgpu")]
203            Device::Wgpu(d) => Ok(Self::Wgpu(dtype.from_data(data), d.clone())),
204        }
205    }
206
207    fn block_size(&self) -> usize {
208        match self {
209            QStorage::Cpu(storage) => storage.block_size(),
210            QStorage::Metal(storage) => storage.dtype().block_size(),
211            QStorage::Cuda(storage) => storage.dtype().block_size(),
212            #[cfg(feature = "rocm")]
213            QStorage::Rocm(storage, _) => storage.block_size(),
214            #[cfg(feature = "vulkan")]
215            QStorage::Vulkan(storage, _) => storage.block_size(),
216            #[cfg(feature = "wgpu")]
217            QStorage::Wgpu(storage, _) => storage.block_size(),
218        }
219    }
220
221    fn dtype(&self) -> GgmlDType {
222        match self {
223            QStorage::Cpu(storage) => storage.dtype(),
224            QStorage::Metal(storage) => storage.dtype(),
225            QStorage::Cuda(storage) => storage.dtype(),
226            #[cfg(feature = "rocm")]
227            QStorage::Rocm(storage, _) => storage.dtype(),
228            #[cfg(feature = "vulkan")]
229            QStorage::Vulkan(storage, _) => storage.dtype(),
230            #[cfg(feature = "wgpu")]
231            QStorage::Wgpu(storage, _) => storage.dtype(),
232        }
233    }
234
235    fn device(&self) -> Device {
236        match self {
237            QStorage::Cpu(_storage) => Device::Cpu,
238            QStorage::Metal(storage) => Device::Metal(storage.device().clone()),
239            QStorage::Cuda(storage) => Device::Cuda(storage.device().clone()),
240            #[cfg(feature = "rocm")]
241            QStorage::Rocm(_storage, device) => Device::Rocm(device.clone()),
242            #[cfg(feature = "vulkan")]
243            QStorage::Vulkan(_storage, device) => Device::Vulkan(device.clone()),
244            #[cfg(feature = "wgpu")]
245            QStorage::Wgpu(_storage, device) => Device::Wgpu(device.clone()),
246        }
247    }
248
249    fn size_in_bytes(&self) -> usize {
250        match self {
251            QStorage::Cpu(storage) => storage.storage_size_in_bytes(),
252            QStorage::Metal(storage) => storage.storage_size_in_bytes(),
253            QStorage::Cuda(storage) => storage.storage_size_in_bytes(),
254            #[cfg(feature = "rocm")]
255            QStorage::Rocm(storage, _) => storage.storage_size_in_bytes(),
256            #[cfg(feature = "vulkan")]
257            QStorage::Vulkan(storage, _) => storage.storage_size_in_bytes(),
258            #[cfg(feature = "wgpu")]
259            QStorage::Wgpu(storage, _) => storage.storage_size_in_bytes(),
260        }
261    }
262
263    fn quantize(&mut self, src: &Storage) -> Result<()> {
264        match (self, src) {
265            (QStorage::Cpu(storage), Storage::Cpu(src)) => {
266                storage.from_float(src.as_slice::<f32>()?);
267            }
268            (QStorage::Metal(storage), Storage::Metal(src)) => storage.quantize(src)?,
269            (QStorage::Cuda(storage), Storage::Cuda(src)) => storage.quantize(src)?,
270            _ => crate::bail!("Invalid quantize storage locations do not match"),
271        }
272        Ok(())
273    }
274
275    fn quantize_imatrix(
276        &mut self,
277        src: &Storage,
278        imatrix_weights: &[f32],
279        n_per_row: usize,
280    ) -> Result<()> {
281        match (self, src) {
282            (QStorage::Cpu(storage), Storage::Cpu(src)) => {
283                storage.from_float_imatrix(src.as_slice::<f32>()?, imatrix_weights, n_per_row);
284            }
285            (QStorage::Metal(storage), Storage::Metal(src)) => {
286                storage.quantize_imatrix(src, imatrix_weights, n_per_row)?
287            }
288            (QStorage::Cuda(storage), Storage::Cuda(src)) => {
289                storage.quantize_imatrix(src, imatrix_weights, n_per_row)?
290            }
291            _ => crate::bail!("Invalid quantize storage locations do not match"),
292        }
293        Ok(())
294    }
295
296    fn quantize_onto(&mut self, src: &Storage) -> Result<()> {
297        match (self, src) {
298            (QStorage::Cpu(storage), Storage::Cpu(src)) => {
299                storage.from_float(src.as_slice::<f32>()?);
300            }
301            (QStorage::Metal(storage), Storage::Cpu(src)) => storage.quantize_onto(src)?,
302            (QStorage::Cuda(storage), Storage::Cpu(src)) => storage.quantize_onto(src)?,
303            _ => crate::bail!("Invalid quantize source storage locations: not on cpu"),
304        }
305        Ok(())
306    }
307
308    fn quantize_imatrix_onto(
309        &mut self,
310        src: &Storage,
311        imatrix_weights: &[f32],
312        n_per_row: usize,
313    ) -> Result<()> {
314        match (self, src) {
315            (QStorage::Cpu(storage), Storage::Cpu(src)) => {
316                storage.from_float_imatrix(src.as_slice::<f32>()?, imatrix_weights, n_per_row);
317            }
318            (QStorage::Metal(storage), Storage::Cpu(src)) => {
319                storage.quantize_imatrix_onto(src, imatrix_weights, n_per_row)?
320            }
321            (QStorage::Cuda(storage), Storage::Cpu(src)) => {
322                storage.quantize_imatrix_onto(src, imatrix_weights, n_per_row)?
323            }
324            _ => crate::bail!("Invalid quantize storage locations do not match"),
325        }
326        Ok(())
327    }
328
329    fn dequantize(&self, elem_count: usize) -> Result<Storage> {
330        match self {
331            QStorage::Cpu(storage) => Ok(Storage::Cpu(storage.dequantize(elem_count)?)),
332            QStorage::Metal(storage) => Ok(Storage::Metal(storage.dequantize(elem_count)?)),
333            QStorage::Cuda(storage) => Ok(Storage::Cuda(storage.dequantize(elem_count)?)),
334            #[cfg(feature = "rocm")]
335            QStorage::Rocm(storage, device) => {
336                // Dequantize on the CPU, then upload the dense f32 weights to the ROCm device.
337                use crate::backend::BackendDevice;
338                let cpu = storage.dequantize(elem_count)?;
339                Ok(Storage::Rocm(device.storage_from_cpu_storage(&cpu)?))
340            }
341            #[cfg(feature = "vulkan")]
342            QStorage::Vulkan(storage, device) => {
343                // Dequantize on the CPU, then upload the f32 weights to the GPU.
344                let cpu = storage.dequantize(elem_count)?;
345                Ok(Storage::Vulkan(device.upload_f32(cpu.as_slice::<f32>()?)?))
346            }
347            #[cfg(feature = "wgpu")]
348            QStorage::Wgpu(storage, device) => {
349                // Dequantize on the CPU, then upload the f32 weights to the GPU.
350                let cpu = storage.dequantize(elem_count)?;
351                Ok(Storage::Wgpu(device.upload_f32(cpu.as_slice::<f32>()?)?))
352            }
353        }
354    }
355
356    fn data(&self) -> Result<Cow<'_, [u8]>> {
357        match self {
358            QStorage::Cpu(storage) => {
359                let data_ptr = storage.as_ptr();
360                let size_in_bytes = storage.storage_size_in_bytes();
361                let data = unsafe { std::slice::from_raw_parts(data_ptr, size_in_bytes) };
362                Ok(Cow::from(data))
363            }
364            QStorage::Cuda(storage) => Ok(Cow::from(storage.data()?)),
365            QStorage::Metal(storage) => Ok(Cow::from(storage.data()?)),
366            #[cfg(feature = "rocm")]
367            QStorage::Rocm(storage, _) => {
368                let data_ptr = storage.as_ptr();
369                let size_in_bytes = storage.storage_size_in_bytes();
370                let data = unsafe { std::slice::from_raw_parts(data_ptr, size_in_bytes) };
371                Ok(Cow::from(data))
372            }
373            #[cfg(feature = "vulkan")]
374            QStorage::Vulkan(storage, _) => {
375                let data_ptr = storage.as_ptr();
376                let size_in_bytes = storage.storage_size_in_bytes();
377                let data = unsafe { std::slice::from_raw_parts(data_ptr, size_in_bytes) };
378                Ok(Cow::from(data))
379            }
380            #[cfg(feature = "wgpu")]
381            QStorage::Wgpu(storage, _) => {
382                let data_ptr = storage.as_ptr();
383                let size_in_bytes = storage.storage_size_in_bytes();
384                let data = unsafe { std::slice::from_raw_parts(data_ptr, size_in_bytes) };
385                Ok(Cow::from(data))
386            }
387        }
388    }
389
390    pub fn device_ptr(&self) -> Result<*const u8> {
391        match self {
392            QStorage::Cuda(storage) => storage.device_ptr(),
393            #[cfg(feature = "rocm")]
394            QStorage::Rocm(..) => crate::bail!("not implemented"),
395            #[cfg(feature = "vulkan")]
396            QStorage::Vulkan(..) => crate::bail!("not implemented"),
397            #[cfg(feature = "wgpu")]
398            QStorage::Wgpu(..) => crate::bail!("not implemented"),
399            QStorage::Metal(_) | QStorage::Cpu(_) => {
400                crate::bail!("not implemented");
401            }
402        }
403    }
404
405    #[cfg(feature = "cuda")]
406    pub fn device_ptr_with_guard<'a>(
407        &'a self,
408        stream: &'a crate::cuda_backend::cudarc::driver::CudaStream,
409    ) -> Result<(
410        *const u8,
411        crate::cuda_backend::cudarc::driver::SyncOnDrop<'a>,
412    )> {
413        match self {
414            QStorage::Cuda(storage) => storage.device_ptr_with_guard(stream),
415            QStorage::Metal(_) | QStorage::Cpu(_) => {
416                crate::bail!("not implemented");
417            }
418        }
419    }
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
423pub enum GgmlDType {
424    F32,
425    F16,
426    BF16,
427    Q4_0,
428    Q4_1,
429    Q5_0,
430    Q5_1,
431    Q8_0,
432    Q8_1,
433    Q2K,
434    Q3K,
435    Q4K,
436    Q5K,
437    Q6K,
438    Q8K,
439    #[allow(non_camel_case_types)]
440    IQ4_NL,
441    #[allow(non_camel_case_types)]
442    IQ4_XS,
443    // 4-bit microscaling float (MXFP4), ggml type 39: 32 elems / block, E8M0 scale + 16 nibble-pairs.
444    MXFP4,
445    // IQ / ternary / 1-bit / NVFP4 codec types (decode-only; dequant->matmul on every backend).
446    #[allow(non_camel_case_types)]
447    IQ2_XXS,
448    #[allow(non_camel_case_types)]
449    IQ2_XS,
450    #[allow(non_camel_case_types)]
451    IQ3_XXS,
452    #[allow(non_camel_case_types)]
453    IQ1_S,
454    #[allow(non_camel_case_types)]
455    IQ3_S,
456    #[allow(non_camel_case_types)]
457    IQ2_S,
458    #[allow(non_camel_case_types)]
459    IQ1_M,
460    TQ1_0,
461    TQ2_0,
462    NVFP4,
463    Q1_0,
464}
465
466// --- Single-source-of-truth wiring (Cut 2) -----------------------------------------
467//
468// The five purely 1:1-per-block `GgmlDType` methods (`from_u32`, `to_u32`, `cpu_zeros`,
469// `from_data`, `type_size`) are generated from the ONE `for_each_quant!` table in
470// `quant_format.rs` instead of six hand-maintained match blocks. Each generator below is
471// handed the whole `Variant => Block @ ggml_id` list and emits the block arms; the three
472// non-block pseudo-types (F32/F16/BF16) that the table intentionally omits keep their
473// bespoke arms inline. Behavior is byte-identical to the previous hand-written matches —
474// the table's ids/blocks were verified equal to them. (`block_size` is left hand-written:
475// the table carries no block-size data and its grouped form — F32=1, Q1_0/NVFP4 special,
476// the big QK_K group — is already minimal.)
477use crate::for_each_quant;
478
479macro_rules! gen_from_u32 {
480    ($($v:ident => $b:ident @ $id:literal),+ $(,)?) => {
481        pub(crate) fn from_u32(u: u32) -> Result<Self> {
482            let dtype = match u {
483                0 => Self::F32,
484                1 => Self::F16,
485                30 => Self::BF16,
486                $( $id => Self::$v, )+
487                _ => crate::bail!("unknown dtype for tensor {u}"),
488            };
489            Ok(dtype)
490        }
491    };
492}
493
494macro_rules! gen_to_u32 {
495    ($($v:ident => $b:ident @ $id:literal),+ $(,)?) => {
496        /// GGML type id for this dtype. Single source of truth (generated from the
497        /// `for_each_quant!` table); cross-crate callers (e.g. hanzo-quant UQFF
498        /// serialization) use this instead of hand-rolled id maps that drift.
499        pub fn to_u32(self) -> u32 {
500            match self {
501                Self::F32 => 0,
502                Self::F16 => 1,
503                Self::BF16 => 30,
504                $( Self::$v => $id, )+
505            }
506        }
507    };
508}
509
510macro_rules! gen_cpu_zeros {
511    ($($v:ident => $b:ident @ $id:literal),+ $(,)?) => {
512        /// The block dtype
513        pub fn cpu_zeros(&self, elem_count: usize) -> Box<dyn QuantizedType> {
514            match self {
515                Self::F32 => Box::new(vec![f32::zeros(); elem_count]),
516                Self::F16 => Box::new(vec![f16::zeros(); elem_count]),
517                Self::BF16 => Box::new(vec![bf16::zeros(); elem_count]),
518                $( Self::$v => Box::new(vec![<$b>::zeros(); elem_count / <$b>::BLCK_SIZE]), )+
519            }
520        }
521    };
522}
523
524macro_rules! gen_from_data {
525    ($($v:ident => $b:ident @ $id:literal),+ $(,)?) => {
526        pub fn from_data(&self, data: Cow<'_, [u8]>) -> Box<dyn QuantizedType> {
527            match self {
528                Self::F32 => Box::new(as_t_slice::<f32>(&data).to_vec()),
529                Self::F16 => Box::new(as_t_slice::<f16>(&data).to_vec()),
530                Self::BF16 => Box::new(as_t_slice::<bf16>(&data).to_vec()),
531                $( Self::$v => Box::new(as_t_slice::<$b>(&data).to_vec()), )+
532            }
533        }
534    };
535}
536
537macro_rules! gen_type_size {
538    ($($v:ident => $b:ident @ $id:literal),+ $(,)?) => {
539        /// The type size for blocks in bytes.
540        pub fn type_size(&self) -> usize {
541            use k_quants::*;
542            match self {
543                Self::F32 => 4,
544                Self::F16 | Self::BF16 => 2,
545                $( Self::$v => std::mem::size_of::<$b>(), )+
546            }
547        }
548    };
549}
550
551impl GgmlDType {
552    for_each_quant!(gen_from_u32);
553    for_each_quant!(gen_to_u32);
554    for_each_quant!(gen_cpu_zeros);
555    for_each_quant!(gen_from_data);
556    for_each_quant!(gen_type_size);
557
558    /// The block size, i.e. the number of elements stored in each block.
559    pub fn block_size(&self) -> usize {
560        match self {
561            Self::F32 => 1,
562            Self::F16 | Self::BF16 => 1,
563            Self::Q4_0 => k_quants::QK4_0,
564            Self::Q4_1 => k_quants::QK4_1,
565            Self::Q5_0 => k_quants::QK5_0,
566            Self::Q5_1 => k_quants::QK5_1,
567            Self::Q8_0 => k_quants::QK8_0,
568            Self::Q8_1 => k_quants::QK8_1,
569            Self::IQ4_NL => k_quants::QK4_NL,
570            Self::MXFP4 => k_quants::QK_MXFP4,
571            Self::Q1_0 => iq_quants::QK1_0,
572            Self::NVFP4 => iq_quants::QK_NVFP4,
573            Self::Q2K
574            | Self::Q3K
575            | Self::Q4K
576            | Self::Q5K
577            | Self::Q6K
578            | Self::Q8K
579            | Self::IQ4_XS
580            | Self::IQ2_XXS
581            | Self::IQ2_XS
582            | Self::IQ3_XXS
583            | Self::IQ1_S
584            | Self::IQ3_S
585            | Self::IQ2_S
586            | Self::IQ1_M
587            | Self::TQ1_0
588            | Self::TQ2_0 => k_quants::QK_K,
589        }
590    }
591}
592
593// A version of GgmlType without `vec_dot` so that it can be dyn boxed.
594pub trait QuantizedType: Send + Sync {
595    fn dtype(&self) -> GgmlDType;
596    fn matmul_t(&self, mkn: (usize, usize, usize), lhs: &[f32], dst: &mut [f32]) -> Result<()>;
597    fn matmul_t_f16(&self, mkn: (usize, usize, usize), lhs: &[f16], dst: &mut [f16]) -> Result<()>;
598    fn dequantize(&self, elem_count: usize) -> Result<CpuStorage>;
599    fn storage_size_in_bytes(&self) -> usize;
600    fn as_ptr(&self) -> *const u8;
601    fn block_size(&self) -> usize;
602    #[allow(clippy::wrong_self_convention)]
603    fn from_float(&mut self, xs: &[f32]);
604    #[allow(clippy::wrong_self_convention)]
605    fn from_float_imatrix(&mut self, xs: &[f32], imatrix_weights: &[f32], n_per_row: usize);
606    fn size(&self) -> usize;
607}
608
609impl<T: k_quants::GgmlType + Send + Sync> QuantizedType for Vec<T> {
610    fn matmul_t(&self, mkn: (usize, usize, usize), lhs: &[f32], dst: &mut [f32]) -> Result<()> {
611        k_quants::matmul(mkn, lhs, self.as_slice(), dst)
612    }
613    fn matmul_t_f16(&self, mkn: (usize, usize, usize), lhs: &[f16], dst: &mut [f16]) -> Result<()> {
614        k_quants::matmul_f16(mkn, lhs, self.as_slice(), dst)
615    }
616
617    fn size(&self) -> usize {
618        self.len() * core::mem::size_of::<T>()
619    }
620
621    fn from_float(&mut self, xs: &[f32]) {
622        T::from_float(xs, self)
623    }
624
625    fn from_float_imatrix(&mut self, xs: &[f32], imatrix_weights: &[f32], n_per_row: usize) {
626        T::from_float_imatrix(xs, self, imatrix_weights, n_per_row)
627    }
628
629    fn dtype(&self) -> GgmlDType {
630        T::DTYPE
631    }
632
633    fn block_size(&self) -> usize {
634        T::BLCK_SIZE
635    }
636
637    fn dequantize(&self, elem_count: usize) -> Result<CpuStorage> {
638        let mut ys = vec![0.0f32; elem_count];
639        T::to_float(self.as_slice(), &mut ys);
640        Ok(CpuStorage::F32(ys))
641    }
642
643    fn storage_size_in_bytes(&self) -> usize {
644        self.len() * std::mem::size_of::<T>()
645    }
646
647    fn as_ptr(&self) -> *const u8 {
648        self.as_ptr() as *const u8
649    }
650}
651
652impl std::fmt::Debug for QTensor {
653    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
654        write!(f, "QTensor[{:?}; {:?}]", self.shape, self.dtype())
655    }
656}
657
658fn check_shape(shape: &Shape, block_size: usize) -> Result<()> {
659    let dims = shape.dims();
660    if dims.is_empty() {
661        crate::bail!("scalar tensor cannot be quantized {shape:?}")
662    }
663    if !dims[dims.len() - 1].is_multiple_of(block_size) {
664        crate::bail!(
665            "quantized tensor must have their last dim divisible by block size {shape:?} {}",
666            block_size
667        )
668    }
669    Ok(())
670}
671
672impl QTensor {
673    pub fn new<S: Into<Shape>>(storage: QStorage, shape: S) -> Result<Self> {
674        let shape = shape.into();
675        check_shape(&shape, storage.block_size())?;
676        Ok(Self { storage, shape })
677    }
678
679    pub fn quantize(src: &Tensor, dtype: GgmlDType) -> Result<Self> {
680        let shape = src.shape();
681        let block_size = dtype.block_size();
682        check_shape(shape, block_size)?;
683        let src = src.to_dtype(crate::DType::F32)?.flatten_all()?;
684        let elem_count = shape.elem_count();
685        if !elem_count.is_multiple_of(block_size) {
686            crate::bail!(
687                "tensor size ({shape:?}) is not divisible by block size {}",
688                block_size
689            )
690        }
691        let mut storage = src.device().qzeros(elem_count, dtype)?;
692        storage.quantize(&src.storage())?;
693        Ok(Self {
694            storage,
695            shape: shape.clone(),
696        })
697    }
698
699    pub fn quantize_imatrix(
700        src: &Tensor,
701        imatrix_weights: &[f32],
702        dtype: GgmlDType,
703    ) -> Result<Self> {
704        // (n_per_row/QK_K-1)*QK_K+(QK_K/32-1)*32+32=n_per_row
705        // Size of imatrix == last dim of tensor
706        let n_per_row = src.dim(D::Minus1)?;
707        if imatrix_weights.len() != n_per_row {
708            crate::bail!(
709                "imatrix weights must have the same length {} as the last dim of src {}",
710                imatrix_weights.len(),
711                src.dim(D::Minus1)?
712            );
713        }
714
715        let shape = src.shape();
716        let block_size = dtype.block_size();
717        check_shape(shape, block_size)?;
718        let src = src.to_dtype(crate::DType::F32)?.flatten_all()?;
719        let elem_count = shape.elem_count();
720        if !elem_count.is_multiple_of(block_size) {
721            crate::bail!(
722                "tensor size ({shape:?}) is not divisible by block size {}",
723                block_size
724            );
725        }
726        let mut storage = src.device().qzeros(elem_count, dtype)?;
727        storage.quantize_imatrix(&src.storage(), imatrix_weights, n_per_row)?;
728        Ok(Self {
729            storage,
730            shape: shape.clone(),
731        })
732    }
733
734    /// Quantize `src` (currently on the CPU) to a QTensor on `dev`
735    pub fn quantize_imatrix_onto(
736        src: &Tensor,
737        imatrix_weights: &[f32],
738        dtype: GgmlDType,
739        dev: &Device,
740    ) -> Result<Self> {
741        if !src.device().is_cpu() {
742            crate::bail!(
743                "`quantize_onto` expects a `src` to be on the cpu, got {:?}.",
744                src.device()
745            )
746        }
747        // (n_per_row/QK_K-1)*QK_K+(QK_K/32-1)*32+32=n_per_row
748        // Size of imatrix == last dim of tensor
749        let n_per_row = src.dim(D::Minus1)?;
750        if imatrix_weights.len() != n_per_row {
751            crate::bail!(
752                "imatrix weights must have the same length {} as the last dim of src {}",
753                imatrix_weights.len(),
754                src.dim(D::Minus1)?
755            );
756        }
757        let shape = src.shape();
758        let block_size = dtype.block_size();
759        check_shape(shape, block_size)?;
760        let src = src.to_dtype(crate::DType::F32)?.flatten_all()?;
761        let elem_count = shape.elem_count();
762        if !elem_count.is_multiple_of(block_size) {
763            crate::bail!(
764                "tensor size ({shape:?}) is not divisible by block size {}",
765                block_size
766            )
767        }
768        // storage is on the `dev`, src is on `cpu`
769        let mut storage = dev.qzeros(elem_count, dtype)?;
770        storage.quantize_imatrix_onto(&src.storage(), imatrix_weights, n_per_row)?;
771        Ok(Self {
772            storage,
773            shape: shape.clone(),
774        })
775    }
776
777    /// Quantize `src` (currently on the CPU) to a QTensor on `dev`
778    pub fn quantize_onto(src: &Tensor, dtype: GgmlDType, dev: &Device) -> Result<Self> {
779        if !src.device().is_cpu() {
780            crate::bail!(
781                "`quantize_onto` expects a `src` to be on the cpu, got {:?}.",
782                src.device()
783            )
784        }
785        let shape = src.shape();
786        let block_size = dtype.block_size();
787        check_shape(shape, block_size)?;
788        let src = src.to_dtype(crate::DType::F32)?.flatten_all()?;
789        let elem_count = shape.elem_count();
790        if !elem_count.is_multiple_of(block_size) {
791            crate::bail!(
792                "tensor size ({shape:?}) is not divisible by block size {}",
793                block_size
794            )
795        }
796        // storage is on the `dev`, src is on `cpu`
797        let mut storage = dev.qzeros(elem_count, dtype)?;
798        storage.quantize_onto(&src.storage())?;
799        Ok(Self {
800            storage,
801            shape: shape.clone(),
802        })
803    }
804
805    pub fn dtype(&self) -> GgmlDType {
806        self.storage.dtype()
807    }
808
809    pub fn device(&self) -> Device {
810        self.storage.device()
811    }
812
813    pub fn rank(&self) -> usize {
814        self.shape.rank()
815    }
816
817    pub fn shape(&self) -> &Shape {
818        &self.shape
819    }
820
821    pub fn dequantize(&self, device: &Device) -> Result<Tensor> {
822        let storage = self.storage.dequantize(self.shape.elem_count())?;
823        let none = crate::op::BackpropOp::none();
824        crate::tensor::from_storage(storage, self.shape.clone(), none, false).to_device(device)
825    }
826
827    pub fn dequantize_f16(&self, device: &Device) -> Result<Tensor> {
828        // In the CUDA case, we have a specialized kernel as this can be useful for volta
829        // architectures. https://github.com/hanzoai/ml/issues/2136
830        match &self.storage {
831            QStorage::Cuda(s) => {
832                let s = s.dequantize_f16(self.shape.elem_count())?;
833                let none = crate::op::BackpropOp::none();
834                crate::tensor::from_storage(Storage::Cuda(s), self.shape.clone(), none, false)
835                    .to_device(device)
836            }
837            _ => {
838                let s = self.dequantize(device)?.to_dtype(crate::DType::F16)?;
839                Ok(s)
840            }
841        }
842    }
843
844    pub fn storage_size_in_bytes(&self) -> usize {
845        self.storage.size_in_bytes()
846    }
847
848    pub fn data(&self) -> Result<Cow<'_, [u8]>> {
849        self.storage.data()
850    }
851
852    // Upload this expert bank's GGML bytes to VRAM ONCE and keep it resident, keyed by the stable
853    // (ptr,len) of the QTensor's CPU bytes. Re-uploading the multi-GB bank per token/layer would
854    // dominate decode, so the cache makes MoE bandwidth-bound on the quant matvec, not the H2D copy.
855    #[cfg(feature = "rocm")]
856    fn rocm_moe_bank(
857        &self,
858        dev: &crate::RocmDevice,
859    ) -> Result<std::sync::Arc<crate::RocmStorage>> {
860        use crate::backend::BackendDevice;
861        let bank = self.data()?;
862        let key = (bank.as_ref().as_ptr() as usize, bank.as_ref().len());
863        let cache = rocm_moe_bank_cache();
864        let mut guard = cache.lock().expect("moe bank cache lock");
865        if let Some(w) = guard.get(&key) {
866            Ok(w.clone())
867        } else {
868            let w = std::sync::Arc::new(dev.storage_from_slice(bank.as_ref())?);
869            guard.insert(key, w.clone());
870            Ok(w)
871        }
872    }
873
874    pub fn indexed_moe_forward(&self, x: &Tensor, ids: &Tensor) -> Result<Tensor> {
875        // The fused CUDA path reads ids as a flat row-major [batch*topk] buffer (rank-agnostic since
876        // 0.11.17); force dense strides so a non-contiguous router output can't misindex the kernel.
877        let ids = &ids.contiguous()?;
878        match &self.storage {
879            // Only dtypes with a fused CUDA indexed-MoE kernel take the fast path; others (e.g. MXFP4,
880            // i-quant/ternary) fall through to the generic per-expert path below, which dequantizes via
881            // QMatMul. The supported set is derived from the ONE kernel-name table (cuda.rs), so this gate
882            // can't drift from the kernels that actually exist -- which is exactly what had stranded
883            // Q4_0/Q4_1/Q5_0/Q5_1 on the CPU even though their fused kernels are now compiled.
884            QStorage::Cuda(s) if cuda::QCudaStorage::supports_indexed_moe(s.dtype()) =>
885            {
886                match (&*x.storage(), &*ids.storage()) {
887                (Storage::Cuda(x_storage), Storage::Cuda(ids_storage)) => {
888                    let (storage, out_shape) = s.indexed_moe_forward(
889                        self.shape(),
890                        x_storage,
891                        x.layout(),
892                        ids_storage,
893                        ids.layout(),
894                    )?;
895                    Ok(crate::tensor::from_storage(
896                        Storage::Cuda(storage),
897                        out_shape,
898                        crate::op::BackpropOp::none(),
899                        false,
900                    ))
901                }
902                _ => {
903                    panic!("Non-cuda indexed_moe_forward is not implemented!");
904                }
905                }
906            },
907            // Native CUDA i-quant MoE: the i-quant codebook types have no Blue-C fused q8_1 MoE kernel,
908            // but a native dp4a MoE-decode kernel (moe_qmatvec_dp4a_<iq*>). The [E,n,k] bank stays
909            // RESIDENT in VRAM and the router gather runs on-device -- the dp4a twin of the ROCm path.
910            // This intercepts i-quant MoE BEFORE the generic fallback below, which would DtoH the whole
911            // expert bank to host (self.data()) and re-upload every selected expert PER TOKEN.
912            #[cfg(feature = "cuda")]
913            QStorage::Cuda(s)
914                if cuda::QCudaStorage::supports_iquant_moe(s.dtype())
915                    && !cuda::iq_dequant_fallback() =>
916            {
917                let out_dtype = x.dtype();
918                let (_e_cnt, n, k) = self.shape().dims3()?;
919                let (t, topk) = ids.dims2()?;
920                let nrows = t * topk;
921
922                // PREFILL (t>1): expert-grouped int8-WMMA MMQ (qmmq) -- stage each expert's weight ONCE
923                // and amortize it over all its routed tokens via the tensor cores (llama mul_mat_id),
924                // instead of the per-slot dp4a re-streaming the weight per token. Uses the RAW [t,in1,k]
925                // input (indexed_moe_grouped broadcasts/gathers internally). IQ1_M (no MMQ kernel) returns
926                // None -> the per-slot dp4a below. HANZO_MOE_QMMQ_FALLBACK forces dp4a (A/B).
927                if t > 1 && std::env::var("HANZO_MOE_QMMQ_FALLBACK").is_err() {
928                    let x_f32 = x.to_dtype(crate::DType::F32)?.contiguous()?;
929                    let ids_u32 = ids.to_dtype(crate::DType::U32)?.contiguous()?;
930                    let (xs, _) = x_f32.storage_and_layout();
931                    let xc = match &*xs {
932                        Storage::Cuda(c) => c,
933                        _ => crate::bail!("cuda i-quant MoE: x not on cuda after contiguous()"),
934                    };
935                    let (ids_s, _) = ids_u32.storage_and_layout();
936                    let idc = match &*ids_s {
937                        Storage::Cuda(c) => c,
938                        _ => crate::bail!("cuda i-quant MoE: ids not on cuda"),
939                    };
940                    if let Some((st, sh)) = s.moe_iquant_qmmq(
941                        self.shape(),
942                        xc.as_cuda_slice::<f32>()?,
943                        x.shape(),
944                        &idc.as_cuda_slice::<u32>()?.slice(0..),
945                        ids.shape(),
946                    )? {
947                        return crate::tensor::from_storage(
948                            Storage::Cuda(st),
949                            sh,
950                            crate::op::BackpropOp::none(),
951                            false,
952                        )
953                        .to_dtype(out_dtype);
954                    }
955                }
956
957                // DECODE (t==1) or qmmq-unsupported (IQ1_M): per-slot dp4a. Broadcast the shared gate/up
958                // input across topk to a per-slot [nrows,k] activation. f32-native -> the matvec returns
959                // F32 and the to_dtype below is a no-op for f32 models.
960                let sdim = x.dim(1)?; // 1 (gate/up: shared input) or topk (down: per-slot)
961                let x_exp = if sdim == topk {
962                    x.clone()
963                } else {
964                    x.broadcast_as((t, topk, k))?
965                };
966                let x_flat = x_exp
967                    .reshape((nrows, k))?
968                    .to_dtype(crate::DType::F32)?
969                    .contiguous()?;
970                let ids_flat = ids.reshape((nrows,))?.to_dtype(crate::DType::U32)?.contiguous()?;
971                let (xstore, _) = x_flat.storage_and_layout();
972                let xc = match &*xstore {
973                    Storage::Cuda(c) => c,
974                    _ => crate::bail!("cuda i-quant MoE: x not on cuda after contiguous()"),
975                };
976                let (idstore, _) = ids_flat.storage_and_layout();
977                let idc = match &*idstore {
978                    Storage::Cuda(c) => c,
979                    _ => crate::bail!("cuda i-quant MoE: ids not on cuda"),
980                };
981                let y = s.moe_iquant_dp4a(
982                    &xc.as_cuda_slice::<f32>()?.slice(0..),
983                    &idc.as_cuda_slice::<u32>()?.slice(0..),
984                    nrows,
985                    n,
986                    k,
987                )?;
988                let out = crate::tensor::from_storage(
989                    Storage::Cuda(y),
990                    (nrows, n),
991                    crate::op::BackpropOp::none(),
992                    false,
993                );
994                out.reshape((t, topk, n))?.to_dtype(out_dtype)
995            }
996            // Native Vulkan MoE: one fused grouped quant matvec dispatch reads the per-expert slice
997            // out of the GGML weight bank [E, n, k] resident in VRAM and gathers by the router ids --
998            // the whole expert compute runs on the GPU (no CPU expert loop; the CPU fallback below
999            // would also hit the unimplemented Vulkan index_add). Supported for Q4_0/Q8_0/Q4_K; other
1000            // quant dtypes fall through to the (CPU-bound) generic path.
1001            #[cfg(feature = "vulkan")]
1002            QStorage::Vulkan(_, vk_dev)
1003                if matches!(
1004                    self.storage.dtype(),
1005                    GgmlDType::Q4_0 | GgmlDType::Q8_0 | GgmlDType::Q4K
1006                ) =>
1007            {
1008                let out_dtype = x.dtype();
1009                let (e_cnt, n, k) = self.shape().dims3()?;
1010                let (t, topk) = ids.dims2()?;
1011                let s = x.dim(1)?; // 1 (gate/up: shared input) or topk (down: per-slot)
1012                let x_exp = if s == topk {
1013                    x.clone()
1014                } else {
1015                    x.broadcast_as((t, topk, k))?
1016                };
1017                // [S, k] contiguous f32 on the Vulkan device; S = t*topk routed slots.
1018                let nrows = t * topk;
1019                let x_flat = x_exp
1020                    .reshape((nrows, k))?
1021                    .to_dtype(crate::DType::F32)?
1022                    .contiguous()?;
1023                let ids_vec = ids
1024                    .reshape((nrows,))?
1025                    .to_dtype(crate::DType::U32)?
1026                    .to_vec1::<u32>()?;
1027                // Defend against a stray id (model bug / corrupt router) reading OOB in the bank.
1028                if let Some(&bad) = ids_vec.iter().find(|&&e| e as usize >= e_cnt) {
1029                    crate::bail!("indexed_moe_forward: expert id {bad} >= num_experts {e_cnt}");
1030                }
1031
1032                let kernel = match self.storage.dtype() {
1033                    GgmlDType::Q4_0 => "moe_matvec_q4_0",
1034                    GgmlDType::Q8_0 => "moe_matvec_q8_0",
1035                    GgmlDType::Q4K => "moe_matvec_q4k",
1036                    other => crate::bail!("vulkan MoE: no kernel for {other:?}"),
1037                };
1038                let bank = self.data()?; // raw GGML bytes for all E experts, [E, n, k]
1039                // moe_matvec_q8_0's shader reads the repacked 9-u32/block layout (same as the decode
1040                // matvec_q8 path); Q4_0/Q4_K shaders read the raw GGML bytes directly.
1041                let wbank = match self.storage.dtype() {
1042                    GgmlDType::Q8_0 => vk_dev.quantize_q8_blocks(&bank, e_cnt * n, k)?,
1043                    _ => vk_dev.upload_qweight(&bank)?,
1044                };
1045                let ids_buf = vk_dev.upload_ids(&ids_vec)?;
1046                let y = {
1047                    let (store, _) = x_flat.storage_and_layout();
1048                    let xv = match &*store {
1049                        Storage::Vulkan(v) => v,
1050                        _ => crate::bail!("vulkan MoE: x not on vulkan after contiguous()"),
1051                    };
1052                    vk_dev.moe_matvec_gpu(kernel, &wbank, xv, &ids_buf, nrows, n, k)?
1053                };
1054                let out = crate::tensor::from_storage(
1055                    Storage::Vulkan(y),
1056                    (nrows, n),
1057                    crate::op::BackpropOp::none(),
1058                    false,
1059                );
1060                out.reshape((t, topk, n))?.to_dtype(out_dtype)
1061            }
1062            // Native wgpu MoE: mirror of the Vulkan fused grouped quant matvec dispatch. The GGML
1063            // weight bank [E, n, k] is uploaded once and the router gather + per-expert GEMM run in
1064            // one WGSL dispatch. Supported for Q4_0/Q8_0/Q4_K.
1065            #[cfg(feature = "wgpu")]
1066            QStorage::Wgpu(_, wgpu_dev)
1067                if matches!(
1068                    self.storage.dtype(),
1069                    GgmlDType::Q4_0 | GgmlDType::Q8_0 | GgmlDType::Q4K
1070                ) =>
1071            {
1072                let out_dtype = x.dtype();
1073                let (e_cnt, n, k) = self.shape().dims3()?;
1074                let (t, topk) = ids.dims2()?;
1075                let s = x.dim(1)?; // 1 (gate/up: shared input) or topk (down: per-slot)
1076                let x_exp = if s == topk {
1077                    x.clone()
1078                } else {
1079                    x.broadcast_as((t, topk, k))?
1080                };
1081                let nrows = t * topk;
1082                let x_flat = x_exp
1083                    .reshape((nrows, k))?
1084                    .to_dtype(crate::DType::F32)?
1085                    .contiguous()?;
1086                let ids_vec = ids
1087                    .reshape((nrows,))?
1088                    .to_dtype(crate::DType::U32)?
1089                    .to_vec1::<u32>()?;
1090                if let Some(&bad) = ids_vec.iter().find(|&&e| e as usize >= e_cnt) {
1091                    crate::bail!("indexed_moe_forward: expert id {bad} >= num_experts {e_cnt}");
1092                }
1093                let kernel = match self.storage.dtype() {
1094                    GgmlDType::Q4_0 => "moe_matvec_q4_0",
1095                    GgmlDType::Q8_0 => "moe_matvec_q8_0",
1096                    GgmlDType::Q4K => "moe_matvec_q4k",
1097                    other => crate::bail!("wgpu MoE: no kernel for {other:?}"),
1098                };
1099                let bank = self.data()?; // raw GGML bytes for all E experts, [E, n, k]
1100                let wbank = wgpu_dev.upload_qweight(&bank)?;
1101                let ids_buf = wgpu_dev.upload_ids(&ids_vec)?;
1102                let y = {
1103                    let (store, _) = x_flat.storage_and_layout();
1104                    let xv = match &*store {
1105                        Storage::Wgpu(v) => v,
1106                        _ => crate::bail!("wgpu MoE: x not on wgpu after contiguous()"),
1107                    };
1108                    wgpu_dev.moe_matvec_gpu(kernel, &wbank, xv, &ids_buf, nrows, n, k)?
1109                };
1110                let out = crate::tensor::from_storage(
1111                    Storage::Wgpu(y),
1112                    (nrows, n),
1113                    crate::op::BackpropOp::none(),
1114                    false,
1115                );
1116                out.reshape((t, topk, n))?.to_dtype(out_dtype)
1117            }
1118            // Native ROCm MoE: the GGML expert bank [E,n,k] is uploaded once and each routed slot
1119            // is dispatched through the SAME unified qmatvec_core<WTYPE> as ordinary decode (no
1120            // MoE-per-quant kernel; works for every wired quant). Avoids ROCm's missing index_add:
1121            // each routed slot writes exactly one output row, placed directly by slot index.
1122            #[cfg(feature = "rocm")]
1123            QStorage::Rocm(_, rocm_dev)
1124                if crate::RocmQuantType::from_ggml(self.storage.dtype()).is_some() =>
1125            {
1126                let qt = crate::RocmQuantType::from_ggml(self.storage.dtype()).unwrap();
1127                let out_dtype = x.dtype();
1128                // e_cnt is not read: ids stay on-device and the router guarantees the bound, so the
1129                // old host bounds check (and its DtoH `to_vec1`) is gone.
1130                let (_e_cnt, n, k) = self.shape().dims3()?;
1131                let (t, topk) = ids.dims2()?;
1132                let s = x.dim(1)?; // 1 (gate/up: shared input) or topk (down: per-slot)
1133                let x_exp = if s == topk {
1134                    x.clone()
1135                } else {
1136                    x.broadcast_as((t, topk, k))?
1137                };
1138                let nrows = t * topk;
1139                // PREFILL (t>1) routes to the fused expert-grouped WMMA GEMM (f16 activations); DECODE
1140                // (t==1) keeps the model's native bf16/f16 on the capture-clean matvec. See the twin in
1141                // QStorage::indexed_moe_forward. HANZO_MOE_QMMQ_FALLBACK forces matvec for the A/B.
1142                // Decode-only types (no qmmq kernel) ride the per-slot matvec core for prefill too
1143                // (correct at any token count). ONE predicate gates every prefill site.
1144                let use_qmmq =
1145                    t > 1 && qt.qmmq_capable() && std::env::var("HANZO_MOE_QMMQ_FALLBACK").is_err();
1146                let x_flat = match x_exp.dtype() {
1147                    // qmmq quantizes f16/f32 activations natively, so keep the model's dtype and skip
1148                    // the f32->f16 cast (a 16.7M-elem read+write per gate/up). Other dtypes (bf16 with
1149                    // a symmetric expert type) still cast to f16.
1150                    DType::F16 | DType::F32 if use_qmmq => x_exp.reshape((nrows, k))?.contiguous()?,
1151                    _ if use_qmmq => x_exp.reshape((nrows, k))?.to_dtype(DType::F16)?.contiguous()?,
1152                    DType::BF16 | DType::F16 => x_exp.reshape((nrows, k))?.contiguous()?,
1153                    // DECODE f32-native: dp4a experts quantize q8_1 from f32 and store f32, so an F32
1154                    // routed activation stays F32 (the matvec returns F32 -> the .to_dtype(out_dtype)
1155                    // below is a no-op), removing the cast pair that wrapped each gate/up/down matvec.
1156                    DType::F32 if qt.dp4a_active() => x_exp.reshape((nrows, k))?.contiguous()?,
1157                    _ => x_exp.reshape((nrows, k))?.to_dtype(DType::F16)?.contiguous()?,
1158                };
1159                let wbank = self.rocm_moe_bank(rocm_dev)?;
1160                // Keep router ids ON the GPU for EVERY wired quant type and run ONE batched launch
1161                // (experts on grid.y, ids read on-device). No `to_vec1` DtoH sync -- that host round-
1162                // trip (3 per layer x 48 layers per token) was both the dominant WSL decode stall AND
1163                // what made HIP stream capture illegal (hipErrorStreamCaptureImplicit -> the graph-path
1164                // SIGSEGV). The router emits a top-k over the e_cnt expert logits, so 0 <= id < e_cnt
1165                // by construction; the prior host bounds check is dropped to stay capture-clean.
1166                let ids_u32 = ids
1167                    .reshape((nrows,))?
1168                    .to_dtype(crate::DType::U32)?
1169                    .contiguous()?;
1170                let (store, _) = x_flat.storage_and_layout();
1171                let xr = match &*store {
1172                    crate::Storage::Rocm(r) => r,
1173                    _ => crate::bail!("rocm MoE: x not on rocm after contiguous()"),
1174                };
1175                let (idstore, _) = ids_u32.storage_and_layout();
1176                let idr = match &*idstore {
1177                    crate::Storage::Rocm(r) => r,
1178                    _ => crate::bail!("rocm MoE: ids not on rocm"),
1179                };
1180                let y = if use_qmmq {
1181                    rocm_dev.moe_qmmq_quant(qt, wbank.as_ref(), xr, idr, nrows, n, k)?
1182                } else {
1183                    rocm_dev.moe_matvec_quant(qt, wbank.as_ref(), xr, idr, nrows, n, k)?
1184                };
1185                let out = crate::tensor::from_storage(
1186                    crate::Storage::Rocm(y),
1187                    (nrows, n),
1188                    crate::op::BackpropOp::none(),
1189                    false,
1190                );
1191                out.reshape((t, topk, n))?.to_dtype(out_dtype)
1192            }
1193            _ => {
1194                // CPU / non-CUDA fallback: per-expert quantized matmul. The packed expert bank
1195                // [E, n, k] is sliced into equal, contiguous per-expert quantized blocks; for
1196                // each expert that is actually selected we run hanzo-ml's native quantized matmul
1197                // on just the tokens routed to it. Nothing is dequantized, so quantized MoE runs
1198                // on any backend (CPU, Metal, ...) at a cost proportional to the active experts.
1199                use crate::Module; // brings QMatMul::forward into scope
1200                use std::collections::HashMap;
1201                use std::sync::Arc;
1202                let device = x.device();
1203                let out_dtype = x.dtype();
1204                let (e_cnt, n, k) = self.shape().dims3()?;
1205                let (t, topk) = ids.dims2()?;
1206                let s = x.dim(1)?; // 1 (gate/up: shared input) or topk (down: per-slot)
1207                let x_exp = if s == topk {
1208                    x.clone()
1209                } else {
1210                    x.broadcast_as((t, topk, k))?
1211                };
1212                let x_flat = x_exp
1213                    .reshape((t * topk, k))?
1214                    .to_dtype(crate::DType::F32)?
1215                    .contiguous()?;
1216                let ids_flat = ids.reshape((t * topk,))?.to_dtype(crate::DType::U32)?;
1217                let ids_vec = ids_flat.to_vec1::<u32>()?;
1218                let mut groups: HashMap<u32, Vec<u32>> = HashMap::new();
1219                for (slot, eid) in ids_vec.iter().enumerate() {
1220                    groups.entry(*eid).or_default().push(slot as u32);
1221                }
1222                let dtype = self.storage.dtype();
1223                let all_bytes = self.data()?;
1224                let expert_bytes = all_bytes.len() / e_cnt;
1225                let mut out_flat = Tensor::zeros((t * topk, n), crate::DType::F32, device)?;
1226                for (eid, slots) in groups.into_iter() {
1227                    let off = eid as usize * expert_bytes;
1228                    let qs = QStorage::from_data(
1229                        std::borrow::Cow::Borrowed(&all_bytes[off..off + expert_bytes]),
1230                        device,
1231                        dtype,
1232                    )?;
1233                    let shape: crate::Shape = (n, k).into();
1234                    let w_e = QTensor { storage: qs, shape };
1235                    let qm = QMatMul::from_arc(Arc::new(w_e))?;
1236                    let m = slots.len();
1237                    let idx = Tensor::from_vec(slots, (m,), device)?;
1238                    let x_e = x_flat.index_select(&idx, 0)?; // [m, k]
1239                    let y_e = qm.forward(&x_e)?.to_dtype(crate::DType::F32)?; // [m, n]
1240                    out_flat = out_flat.index_add(&idx, &y_e, 0)?;
1241                }
1242                out_flat.reshape((t, topk, n))?.to_dtype(out_dtype)
1243            }
1244        }
1245    }
1246
1247    pub fn device_ptr(&self) -> Result<*const u8> {
1248        match &self.storage {
1249            QStorage::Cuda(storage) => storage.device_ptr(),
1250            #[cfg(feature = "rocm")]
1251            QStorage::Rocm(..) => crate::bail!("not implemented"),
1252            #[cfg(feature = "vulkan")]
1253            QStorage::Vulkan(..) => crate::bail!("not implemented"),
1254            #[cfg(feature = "wgpu")]
1255            QStorage::Wgpu(..) => crate::bail!("not implemented"),
1256            QStorage::Metal(_) | QStorage::Cpu(_) => {
1257                crate::bail!("not implemented");
1258            }
1259        }
1260    }
1261
1262    #[cfg(feature = "cuda")]
1263    pub fn device_ptr_with_guard<'a>(
1264        &'a self,
1265        stream: &'a crate::cuda_backend::cudarc::driver::CudaStream,
1266    ) -> Result<(
1267        *const u8,
1268        crate::cuda_backend::cudarc::driver::SyncOnDrop<'a>,
1269    )> {
1270        self.storage.device_ptr_with_guard(stream)
1271    }
1272}
1273
1274#[derive(Clone, Debug)]
1275pub enum QMatMul {
1276    QTensor(std::sync::Arc<QTensor>),
1277    Tensor(Tensor),
1278    TensorF16(Tensor),
1279    // Native Vulkan quantized weight: the GGML quantized blocks live in VRAM (Q4_0/Q4_K ~0.5 B/elem,
1280    // Q8_0 ~1.06 B/elem). Decode (1 row) runs the matching on-GPU quant matvec kernel directly out of
1281    // the block format (no CPU dequant, no re-pack) -- the bandwidth lever for memory-bound decode.
1282    // Prefill (>1 row) dequantizes the original `qtensor` to a temporary f32 weight. `dtype` selects
1283    // the kernel; `n`/`k` are the weight dims.
1284    #[cfg(feature = "vulkan")]
1285    VulkanQuant {
1286        qtensor: std::sync::Arc<QTensor>,
1287        wq: std::sync::Arc<crate::VulkanStorage>,
1288        dtype: GgmlDType,
1289        n: usize,
1290        k: usize,
1291    },
1292    // wgpu mirror of VulkanQuant: GGML quantized blocks live in VRAM and decode (1 row) runs the
1293    // matching native-GGML quant matvec WGSL kernel straight out of the block format. Prefill (>1
1294    // row) dequantizes to a temporary f32 weight. `dtype` selects the kernel; `n`/`k` are the dims.
1295    #[cfg(feature = "wgpu")]
1296    WgpuQuant {
1297        qtensor: std::sync::Arc<QTensor>,
1298        wq: std::sync::Arc<crate::WgpuStorage>,
1299        dtype: GgmlDType,
1300        n: usize,
1301        k: usize,
1302    },
1303    // Native ROCm quantized weight: the GGML blocks live in VRAM. Decode (1 row) runs the ONE
1304    // unified on-GPU quant matvec (qmatvec_core<WTYPE>) straight out of the block format; prefill
1305    // (>1 row) runs the ONE unified int8 WMMA GEMM (qmmq_core<WTYPE>) -- both for the full wired
1306    // spread (Q8_0/Q4_0/Q4_K/Q6_K/IQ4_XS/TQ2_0). Unwired types dequantize to a temporary f16
1307    // weight (RDNA matrix-core matmul). `n`/`k` are the weight dims.
1308    #[cfg(feature = "rocm")]
1309    RocmQuant {
1310        qtensor: std::sync::Arc<QTensor>,
1311        wq: std::sync::Arc<crate::RocmStorage>,
1312        dtype: GgmlDType,
1313        n: usize,
1314        k: usize,
1315    },
1316}
1317
1318// Resident ROCm MoE expert-bank cache: maps a (CPU bank ptr, len) to its uploaded VRAM copy so
1319// the multi-GB GGML expert bank is host->device copied ONCE, not per token/layer. The CPU bytes
1320// are owned by the model's QTensor for its lifetime, so the pointer is a stable key. Keyed by
1321// usize (raw ptr) to stay Send+Sync; the RocmStorage is reference-counted and reused.
1322#[cfg(feature = "rocm")]
1323fn rocm_moe_bank_cache(
1324) -> &'static std::sync::Mutex<std::collections::HashMap<(usize, usize), std::sync::Arc<crate::RocmStorage>>>
1325{
1326    static CACHE: std::sync::OnceLock<
1327        std::sync::Mutex<std::collections::HashMap<(usize, usize), std::sync::Arc<crate::RocmStorage>>>,
1328    > = std::sync::OnceLock::new();
1329    CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
1330}
1331
1332/// FUSED MoE expert-combine: `out[i,j] = sum_e scores[i,e] * ys[i,e,j]`, reducing the per-expert
1333/// outputs `ys` [t, topk, n] by the router weights `scores` [t, topk] into [t, n]. On ROCm this is
1334/// ONE fused kernel (`RocmDevice::moe_combine`) instead of `ys.broadcast_mul(scores).sum(Minus2)`,
1335/// which cast ys -> f32, wrote a [t,topk,n] f32 product temp, and ran an 8-wide strided reduce.
1336/// Other backends keep the generic broadcast-mul + sum.
1337pub fn moe_combine(ys: &Tensor, scores: &Tensor) -> Result<Tensor> {
1338    let (t, topk, n) = ys.dims3()?;
1339    #[cfg(feature = "rocm")]
1340    if let Device::Rocm(dev) = ys.device() {
1341        if std::env::var("HANZO_MOE_COMBINE_FALLBACK").is_ok() {
1342            return ys.broadcast_mul(&scores.unsqueeze(D::Minus1)?)?.sum(D::Minus2);
1343        }
1344        let ys_c = ys.contiguous()?;
1345        let scores_c = scores.to_dtype(DType::F32)?.contiguous()?;
1346        let (ys_store, _) = ys_c.storage_and_layout();
1347        let yr = match &*ys_store {
1348            Storage::Rocm(r) => r,
1349            _ => crate::bail!("moe_combine: ys not on rocm after contiguous()"),
1350        };
1351        let (sc_store, _) = scores_c.storage_and_layout();
1352        let sr = match &*sc_store {
1353            Storage::Rocm(r) => r,
1354            _ => crate::bail!("moe_combine: scores not on rocm after contiguous()"),
1355        };
1356        let out = dev.moe_combine(yr, sr, t, topk, n)?;
1357        return Ok(crate::tensor::from_storage(
1358            Storage::Rocm(out),
1359            (t, n),
1360            crate::op::BackpropOp::none(),
1361            false,
1362        ));
1363    }
1364    ys.broadcast_mul(&scores.unsqueeze(D::Minus1)?)?.sum(D::Minus2)
1365}
1366
1367/// Fused MoE router. Reduces the F32 router logits [ntok, n_experts] to the topk selected expert
1368/// ids [ntok, topk] (descending logit) and their softmax weights [ntok, topk]; `norm` renormalizes
1369/// the topk weights to sum 1 (norm_topk_prob). On ROCm this is ONE `moe_route` kernel replacing the
1370/// softmax->sort->narrow->sum->div chain (~6 launches/layer); elsewhere it is that chain via ml ops.
1371pub fn moe_route(logits: &Tensor, topk: usize, norm: bool) -> Result<(Tensor, Tensor)> {
1372    let (ntok, n_experts) = logits.dims2()?;
1373    #[cfg(feature = "rocm")]
1374    if let Device::Rocm(dev) = logits.device() {
1375        if std::env::var("HANZO_MOE_ROUTE_FALLBACK").is_err() {
1376            let logits_c = logits.to_dtype(DType::F32)?.contiguous()?;
1377            let (lg_store, _) = logits_c.storage_and_layout();
1378            let lr = match &*lg_store {
1379                Storage::Rocm(r) => r,
1380                _ => crate::bail!("moe_route: logits not on rocm after contiguous()"),
1381            };
1382            let (ids, w) = dev.moe_route(lr, ntok, n_experts, topk, norm)?;
1383            let ids_t = crate::tensor::from_storage(
1384                Storage::Rocm(ids),
1385                (ntok, topk),
1386                crate::op::BackpropOp::none(),
1387                false,
1388            );
1389            let w_t = crate::tensor::from_storage(
1390                Storage::Rocm(w),
1391                (ntok, topk),
1392                crate::op::BackpropOp::none(),
1393                false,
1394            );
1395            return Ok((ids_t, w_t));
1396        }
1397    }
1398    let lf = logits.to_dtype(DType::F32)?;
1399    let mx = lf.max_keepdim(D::Minus1)?;
1400    let e = lf.broadcast_sub(&mx)?.exp()?;
1401    let z = e.sum_keepdim(D::Minus1)?;
1402    let p = e.broadcast_div(&z)?;
1403    let (sv, si) = p.sort_last_dim(false)?;
1404    let ids = si.narrow(D::Minus1, 0, topk)?.contiguous()?;
1405    let mut w = sv.narrow(D::Minus1, 0, topk)?.contiguous()?;
1406    if norm {
1407        w = w.broadcast_div(&w.sum_keepdim(D::Minus1)?)?;
1408    }
1409    Ok((ids, w))
1410}
1411
1412/// Fused MoE gate+up projections. Both expert banks consume the SAME routed token `x` [t,1,k], so the
1413/// shared input is broadcast + quantized ONCE and matvec'd against both banks (vs once per bank,
1414/// re-materializing + re-quantizing the identical activation). Returns the raw (gate_out, up_out)
1415/// [t,topk,n]; the caller applies silu(gate)*up. Each output is bit-identical to the unfused
1416/// `indexed_moe_forward` because the q8_1 activation is deterministic in `x`. ROCm decode/matvec path
1417/// only (the prefill qmmq path keeps its own per-bank quantize); every other case runs the two
1418/// unfused forwards. HANZO_MOE_GATEUP_FALLBACK forces the unfused path (the A/B + equivalence lever).
1419pub fn moe_gate_up(
1420    x: &Tensor,
1421    ids: &Tensor,
1422    gate: &QMatMul,
1423    up: &QMatMul,
1424) -> Result<(Tensor, Tensor)> {
1425    #[cfg(feature = "rocm")]
1426    if std::env::var("HANZO_MOE_GATEUP_FALLBACK").is_err() {
1427        if let (QMatMul::QTensor(gq), QMatMul::QTensor(uq)) = (gate, up) {
1428            if let (QStorage::Rocm(_, dev), QStorage::Rocm(..)) = (&gq.storage, &uq.storage) {
1429                let dt = gq.storage.dtype();
1430                if dt == uq.storage.dtype() {
1431                    if let Some(qt) = crate::RocmQuantType::from_ggml(dt) {
1432                        let (_e, n, k) = gq.shape().dims3()?;
1433                        let (t, topk) = ids.dims2()?;
1434                        let use_qmmq = t > 1
1435                            && qt.qmmq_capable()
1436                            && std::env::var("HANZO_MOE_QMMQ_FALLBACK").is_err();
1437                        if x.dim(1)? == 1 && !use_qmmq {
1438                            let nrows = t * topk;
1439                            let x_exp = x.broadcast_as((t, topk, k))?;
1440                            let x_flat = match x_exp.dtype() {
1441                                DType::BF16 | DType::F16 => x_exp.reshape((nrows, k))?.contiguous()?,
1442                                DType::F32 if qt.dp4a_active() => {
1443                                    x_exp.reshape((nrows, k))?.contiguous()?
1444                                }
1445                                _ => {
1446                                    x_exp.reshape((nrows, k))?.to_dtype(DType::F16)?.contiguous()?
1447                                }
1448                            };
1449                            let out_dtype = x.dtype();
1450                            let ids_u32 =
1451                                ids.reshape((nrows,))?.to_dtype(DType::U32)?.contiguous()?;
1452                            let gwb = gq.rocm_moe_bank(dev)?;
1453                            let uwb = uq.rocm_moe_bank(dev)?;
1454                            let (xstore, _) = x_flat.storage_and_layout();
1455                            let xr = match &*xstore {
1456                                Storage::Rocm(r) => r,
1457                                _ => crate::bail!("moe_gate_up: x not on rocm after contiguous()"),
1458                            };
1459                            let (idstore, _) = ids_u32.storage_and_layout();
1460                            let idr = match &*idstore {
1461                                Storage::Rocm(r) => r,
1462                                _ => crate::bail!("moe_gate_up: ids not on rocm"),
1463                            };
1464                            let (gy, uy) = dev.moe_matvec_pair(
1465                                qt,
1466                                gwb.as_ref(),
1467                                uwb.as_ref(),
1468                                xr,
1469                                idr,
1470                                nrows,
1471                                n,
1472                                k,
1473                            )?;
1474                            let g = crate::tensor::from_storage(
1475                                Storage::Rocm(gy),
1476                                (nrows, n),
1477                                crate::op::BackpropOp::none(),
1478                                false,
1479                            )
1480                            .reshape((t, topk, n))?
1481                            .to_dtype(out_dtype)?;
1482                            let u = crate::tensor::from_storage(
1483                                Storage::Rocm(uy),
1484                                (nrows, n),
1485                                crate::op::BackpropOp::none(),
1486                                false,
1487                            )
1488                            .reshape((t, topk, n))?
1489                            .to_dtype(out_dtype)?;
1490                            return Ok((g, u));
1491                        }
1492                    }
1493                }
1494            }
1495        }
1496    }
1497    Ok((gate.indexed_moe_forward(x, ids)?, up.indexed_moe_forward(x, ids)?))
1498}
1499
1500thread_local! {
1501    static DEQUANTIZE_ALL: bool = {
1502        match std::env::var("DEQUANTIZE_ALL") {
1503            Ok(s) => {
1504                !s.is_empty() && s != "0"
1505            },
1506            Err(_) => false,
1507        }
1508    }
1509}
1510
1511thread_local! {
1512    static DEQUANTIZE_ALL_F16: bool = {
1513        match std::env::var("DEQUANTIZE_ALL_F16") {
1514            Ok(s) => {
1515                !s.is_empty() && s != "0"
1516            },
1517            Err(_) => false,
1518        }
1519    }
1520}
1521
1522impl QMatMul {
1523    pub fn from_arc(qtensor: std::sync::Arc<QTensor>) -> Result<Self> {
1524        // Native Vulkan quantized path: keep the GGML quantized blocks in VRAM and run the matching
1525        // on-GPU quant matvec for decode, instead of dequantizing the whole model to f32 (4x the
1526        // decode bandwidth). The kernel reads the GGML block format straight from the uploaded bytes
1527        // -- no CPU dequant, no re-pack -- so this is exact w.r.t. the CPU reference. Q4_0/Q4_K need
1528        // k a multiple of their block (32 / 256); Q8_0 needs a multiple of 32.
1529        #[cfg(feature = "vulkan")]
1530        {
1531            let dt = qtensor.dtype();
1532            let native_vk = matches!(
1533                dt,
1534                GgmlDType::Q4_0
1535                    | GgmlDType::Q8_0
1536                    | GgmlDType::Q4K
1537                    | GgmlDType::Q5K
1538                    | GgmlDType::Q6K
1539                    | GgmlDType::Q2K
1540                    | GgmlDType::Q3K
1541                    | GgmlDType::IQ4_XS
1542                    | GgmlDType::IQ4_NL
1543                    | GgmlDType::TQ2_0
1544                    | GgmlDType::IQ2_XXS
1545                    | GgmlDType::IQ2_S
1546                    | GgmlDType::IQ3_XXS
1547                    | GgmlDType::IQ3_S
1548                    | GgmlDType::IQ1_S
1549                    | GgmlDType::IQ1_M
1550                    | GgmlDType::IQ2_XS
1551            );
1552            if native_vk {
1553                if let Device::Vulkan(d) = qtensor.device() {
1554                    if let Ok((n, k)) = qtensor.shape().dims2() {
1555                        let blk = dt.block_size();
1556                        if k % blk == 0 {
1557                            let bytes = qtensor.data()?;
1558                            // Q6_K (210 B) and Q3_K (110 B) blocks are not u32-aligned; their shaders
1559                            // read a padded u32 stride, so repack on upload. Q8_0 repacks to the
1560                            // 9-u32/block layout that BOTH its decode (mul_mat_vec_q8) and prefill GEMM
1561                            // (mul_mat_q8) read -- ONE layout, so decode + prefill + MoE all agree
1562                            // (mirrors how the MoE bank repacks Q8_0). Every other native type's
1563                            // shaders byte-address the raw GGML bytes directly.
1564                            let wq = match dt {
1565                                GgmlDType::Q6K => d.quantize_q6k(&bytes, n, k)?,
1566                                GgmlDType::Q3K => d.quantize_q3k(&bytes, n, k)?,
1567                                GgmlDType::Q8_0 => d.quantize_q8_blocks(&bytes, n, k)?,
1568                                GgmlDType::IQ2_XXS => d.quantize_iq2xxs(&bytes, n, k)?,
1569                                GgmlDType::IQ2_XS => d.quantize_iq2xs(&bytes, n, k)?,
1570                                GgmlDType::IQ1_M => d.quantize_iq1m(&bytes, n, k)?,
1571                                GgmlDType::IQ1_S => d.quantize_iq1s(&bytes, n, k)?,
1572                                GgmlDType::IQ3_S => d.quantize_iq3s(&bytes, n, k)?,
1573                                GgmlDType::IQ3_XXS => d.quantize_iq3xxs(&bytes, n, k)?,
1574                                GgmlDType::IQ2_S => d.quantize_iq2s(&bytes, n, k)?,
1575                                _ => d.upload_qweight(&bytes)?,
1576                            };
1577                            return Ok(Self::VulkanQuant {
1578                                qtensor,
1579                                wq: std::sync::Arc::new(wq),
1580                                dtype: dt,
1581                                n,
1582                                k,
1583                            });
1584                        }
1585                    }
1586                }
1587            }
1588        }
1589        // Native wgpu quantized path: same idea as Vulkan. Ships the Q4_0/Q8_0/Q4_K WGSL matvec
1590        // kernels; other dtypes fall through to the dequantize path below.
1591        #[cfg(feature = "wgpu")]
1592        {
1593            let dt = qtensor.dtype();
1594            let native_wgpu = matches!(dt, GgmlDType::Q4_0 | GgmlDType::Q8_0 | GgmlDType::Q4K);
1595            if native_wgpu {
1596                if let Device::Wgpu(d) = qtensor.device() {
1597                    if let Ok((n, k)) = qtensor.shape().dims2() {
1598                        let blk = dt.block_size();
1599                        if k % blk == 0 {
1600                            let bytes = qtensor.data()?;
1601                            let wq = d.upload_qweight(&bytes)?;
1602                            return Ok(Self::WgpuQuant {
1603                                qtensor,
1604                                wq: std::sync::Arc::new(wq),
1605                                dtype: dt,
1606                                n,
1607                                k,
1608                            });
1609                        }
1610                    }
1611                }
1612            }
1613        }
1614        // Native ROCm quantized path: keep the GGML blocks in VRAM and run the ONE unified on-GPU
1615        // quant decode core (qmatvec_core<WTYPE>; Q8_0+Q4_0 also have the int8 WMMA prefill gemm),
1616        // instead of dequantizing the whole model to dense f16 (2x+ the decode bandwidth). Reads the
1617        // block format straight from the uploaded bytes -- exact w.r.t. the CPU reference. The wired
1618        // set is exactly RocmQuantType::from_ggml; k must be a multiple of that type's block size.
1619        #[cfg(feature = "rocm")]
1620        {
1621            let dt = qtensor.dtype();
1622            // Decode AND prefill native iff the unified core has this type wired (one enum row in
1623            // RocmQuantType): decode rides qmatvec_core<WTYPE>, prefill (rows>1) rides the int8 WMMA
1624            // qmmq_core<WTYPE>. Both cover the SAME wired spread; adding a type is one enum row + the
1625            // in-kernel decode, no per-quant kernel. Unwired types dequantize-to-f16 in forward().
1626            if let Some(qt) = crate::RocmQuantType::from_ggml(dt) {
1627                if let Device::Rocm(d) = qtensor.device() {
1628                    if let Ok((n, k)) = qtensor.shape().dims2() {
1629                        let blk_ok = k % qt.block_elems() == 0;
1630                        if blk_ok {
1631                            use crate::backend::BackendDevice;
1632                            let bytes = qtensor.data()?;
1633                            let wq = d.storage_from_slice(bytes.as_ref())?;
1634                            return Ok(Self::RocmQuant {
1635                                qtensor,
1636                                wq: std::sync::Arc::new(wq),
1637                                dtype: dt,
1638                                n,
1639                                k,
1640                            });
1641                        }
1642                    }
1643                }
1644            }
1645        }
1646        // ROCm MoE bank: a 3D [E,n,k] expert bank of a wired quant type stays QUANTIZED (kept as
1647        // QTensor), so `indexed_moe_forward` runs each routed expert through the ONE unified
1648        // qmatvec_core (no per-expert kernel) instead of dequantizing the whole bank to dense f16
1649        // (which for a 30B-A3B model is many GB of resident f16 AND has no indexed_moe path). The 2D
1650        // RocmQuant decode/prefill path above already handles ordinary weights; this is the MoE case.
1651        #[cfg(feature = "rocm")]
1652        {
1653            if qtensor.device().is_rocm()
1654                && qtensor.shape().dims().len() == 3
1655                && crate::RocmQuantType::from_ggml(qtensor.dtype()).is_some()
1656                && !DEQUANTIZE_ALL.with(|b| *b)
1657            {
1658                return Ok(Self::QTensor(qtensor));
1659            }
1660        }
1661        let dequantize = match qtensor.dtype() {
1662            GgmlDType::F32 | GgmlDType::F16 | GgmlDType::BF16 => true,
1663            // The Vulkan/wgpu/ROCm backends have no generic native quantized matmul, so dequantize
1664            // to f32 here (once, at construction) and run the regular f32 GPU matmul.
1665            _ => {
1666                DEQUANTIZE_ALL.with(|b| *b)
1667                    || qtensor.device().is_vulkan()
1668                    || qtensor.device().is_wgpu()
1669                    || qtensor.device().is_rocm()
1670            }
1671        };
1672        let t = if dequantize {
1673            // ROCm: dequantize to f16 so the matmul hits RDNA3.5 matrix cores (WMMA). Dense f32
1674            // (sgemm) has no matrix-core path on RDNA and runs ~an order of magnitude slower, and
1675            // f16 also halves the resident weight memory.
1676            if qtensor.device().is_rocm() {
1677                Self::TensorF16(qtensor.dequantize_f16(&qtensor.device())?)
1678            } else {
1679                Self::Tensor(qtensor.dequantize(&qtensor.device())?)
1680            }
1681        } else if DEQUANTIZE_ALL_F16.with(|b| *b) {
1682            let tensor = qtensor.dequantize_f16(&qtensor.device())?;
1683            Self::TensorF16(tensor)
1684        } else {
1685            Self::QTensor(qtensor)
1686        };
1687        Ok(t)
1688    }
1689
1690    pub fn from_qtensor(qtensor: QTensor) -> Result<Self> {
1691        Self::from_arc(std::sync::Arc::new(qtensor))
1692    }
1693
1694    pub fn dequantize_f16(&self) -> Result<Tensor> {
1695        match self {
1696            Self::QTensor(t) => t.dequantize_f16(&t.device()),
1697            Self::Tensor(t) => t.to_dtype(DType::F16),
1698            Self::TensorF16(t) => Ok(t.clone()),
1699            #[cfg(feature = "rocm")]
1700            Self::RocmQuant { qtensor, .. } => qtensor.dequantize_f16(&qtensor.device()),
1701            #[cfg(feature = "vulkan")]
1702            Self::VulkanQuant { qtensor, .. } => qtensor.dequantize_f16(&qtensor.device()),
1703            #[cfg(feature = "wgpu")]
1704            Self::WgpuQuant { qtensor, .. } => qtensor.dequantize_f16(&qtensor.device()),
1705        }
1706    }
1707
1708    pub fn forward_via_f16(&self, xs: &Tensor) -> Result<Tensor> {
1709        let w = self.dequantize_f16()?;
1710        let in_dtype = xs.dtype();
1711        let w = match *xs.dims() {
1712            [b1, b2, _, _] => w.broadcast_left((b1, b2))?.t()?,
1713            [bsize, _, _] => w.broadcast_left(bsize)?.t()?,
1714            _ => w.t()?,
1715        };
1716        xs.to_dtype(DType::F16)?.matmul(&w)?.to_dtype(in_dtype)
1717    }
1718
1719    pub fn indexed_moe_forward(&self, x: &Tensor, ids: &Tensor) -> Result<Tensor> {
1720        match self {
1721            Self::QTensor(t) => t.indexed_moe_forward(x, ids),
1722            // Resident-bank MoE: `wq` already holds the [E,n,k] GGML blocks in VRAM (uploaded at
1723            // load), so route straight to the batched on-GPU quant matvec. Delegating to `qtensor`
1724            // (CPU-side) instead drops to the generic fallback that re-uploads every routed expert
1725            // every token -- the 20-50x decode cliff. `qtensor` is read only for its shape.
1726            #[cfg(feature = "rocm")]
1727            Self::RocmQuant {
1728                qtensor, wq, dtype, ..
1729            } if crate::RocmQuantType::from_ggml(*dtype).is_some() => {
1730                let qt = crate::RocmQuantType::from_ggml(*dtype).unwrap();
1731                let wbank = wq.as_ref();
1732                // e_cnt unused: ids stay on-device, router guarantees the bound (no host check).
1733                let (_e_cnt, n, k) = qtensor.shape().dims3()?;
1734                let (t, topk) = ids.dims2()?;
1735                let s = x.dim(1)?; // 1 (gate/up: shared input) or topk (down: per-slot)
1736                let x_exp = if s == topk {
1737                    x.clone()
1738                } else {
1739                    x.broadcast_as((t, topk, k))?
1740                };
1741                let nrows = t * topk;
1742                // PREFILL (t>1, never graph-captured) routes to the FUSED expert-grouped WMMA GEMM
1743                // (`moe_qmmq_quant`), which needs f16 activations; DECODE (t==1) stays on the
1744                // capture-clean dp4a/scalar matvec, which takes bf16/f16 natively. HANZO_MOE_QMMQ_FALLBACK
1745                // forces the matvec on prefill too (the before/after A/B + oracle-equivalence lever).
1746                // Decode-only types (no qmmq kernel) ride the per-slot matvec core for prefill too
1747                // (correct at any token count). ONE predicate gates every prefill site.
1748                let use_qmmq =
1749                    t > 1 && qt.qmmq_capable() && std::env::var("HANZO_MOE_QMMQ_FALLBACK").is_err();
1750                let x_flat = match x_exp.dtype() {
1751                    // qmmq quantizes f16/f32 activations natively, so keep the model's dtype and skip
1752                    // the f32->f16 cast (a 16.7M-elem read+write per gate/up). Other dtypes (bf16 with
1753                    // a symmetric expert type) still cast to f16.
1754                    DType::F16 | DType::F32 if use_qmmq => x_exp.reshape((nrows, k))?.contiguous()?,
1755                    _ if use_qmmq => x_exp.reshape((nrows, k))?.to_dtype(DType::F16)?.contiguous()?,
1756                    DType::BF16 | DType::F16 => x_exp.reshape((nrows, k))?.contiguous()?,
1757                    // DECODE f32-native dp4a: keep F32 routed activation F32 end-to-end (matvec stores
1758                    // F32), eliding the cast pair around each expert matvec. See QStorage twin above.
1759                    DType::F32 if qt.dp4a_active() => x_exp.reshape((nrows, k))?.contiguous()?,
1760                    _ => x_exp.reshape((nrows, k))?.to_dtype(DType::F16)?.contiguous()?,
1761                };
1762                let out_dtype = x.dtype();
1763                // Keep router ids ON the GPU for EVERY wired quant type: the batched kernels index
1764                // experts on-device, so there is no per-call `to_vec1` host round-trip. That DtoH
1765                // sync (3 per layer x 48 layers per token) was both the dominant decode stall on WSL
1766                // AND the HIP-graph capture breaker. Router top-k guarantees 0 <= id < e_cnt.
1767                let ids_u32 = ids
1768                    .reshape((nrows,))?
1769                    .to_dtype(crate::DType::U32)?
1770                    .contiguous()?;
1771                let (xstore, _) = x_flat.storage_and_layout();
1772                let xr = match &*xstore {
1773                    crate::Storage::Rocm(r) => r,
1774                    _ => crate::bail!("rocm MoE: x not on rocm after contiguous()"),
1775                };
1776                let (idstore, _) = ids_u32.storage_and_layout();
1777                let idr = match &*idstore {
1778                    crate::Storage::Rocm(r) => r,
1779                    _ => crate::bail!("rocm MoE: ids not on rocm"),
1780                };
1781                let y = if use_qmmq {
1782                    wbank.device.moe_qmmq_quant(qt, wbank, xr, idr, nrows, n, k)?
1783                } else {
1784                    wbank.device.moe_matvec_quant(qt, wbank, xr, idr, nrows, n, k)?
1785                };
1786                let out = crate::tensor::from_storage(
1787                    crate::Storage::Rocm(y),
1788                    (nrows, n),
1789                    crate::op::BackpropOp::none(),
1790                    false,
1791                );
1792                out.reshape((t, topk, n))?.to_dtype(out_dtype)
1793            }
1794            // Unwired ROCm quant dtypes (no on-GPU quant matvec): CPU per-expert fallback.
1795            #[cfg(feature = "rocm")]
1796            Self::RocmQuant { qtensor, .. } => qtensor.indexed_moe_forward(x, ids),
1797            #[cfg(feature = "vulkan")]
1798            Self::VulkanQuant { qtensor, .. } => qtensor.indexed_moe_forward(x, ids),
1799            #[cfg(feature = "wgpu")]
1800            Self::WgpuQuant { qtensor, .. } => qtensor.indexed_moe_forward(x, ids),
1801            _ => {
1802                panic!("Not implemented!")
1803            }
1804        }
1805    }
1806}
1807
1808impl crate::CustomOp1 for QTensor {
1809    fn name(&self) -> &'static str {
1810        "qmatmul"
1811    }
1812
1813    fn cpu_fwd(
1814        &self,
1815        storage: &crate::CpuStorage,
1816        layout: &crate::Layout,
1817    ) -> Result<(crate::CpuStorage, Shape)> {
1818        if !layout.is_contiguous() {
1819            crate::bail!("input tensor is not contiguous {layout:?}")
1820        }
1821        let src_shape = layout.shape();
1822        // self is transposed so n is first then k.
1823        let (n, k) = self.shape.dims2()?;
1824        if src_shape.rank() < 2 {
1825            crate::bail!("input tensor has only one dimension {layout:?}")
1826        }
1827        let mut dst_shape = src_shape.dims().to_vec();
1828        let last_k = dst_shape.pop().unwrap();
1829        if last_k != k {
1830            crate::bail!("input tensor {layout:?} incompatible with {:?}", self.shape)
1831        }
1832        dst_shape.push(n);
1833        let dst_shape = Shape::from(dst_shape);
1834        #[allow(clippy::infallible_destructuring_match)]
1835        let self_storage = match &self.storage {
1836            QStorage::Cpu(storage) => storage,
1837            #[cfg(feature = "rocm")]
1838            QStorage::Rocm(..) => crate::bail!("Invalid storage"),
1839            #[cfg(feature = "vulkan")]
1840            QStorage::Vulkan(..) => crate::bail!("Invalid storage"),
1841            #[cfg(feature = "wgpu")]
1842            QStorage::Wgpu(..) => crate::bail!("Invalid storage"),
1843            QStorage::Metal(_) | QStorage::Cuda(_) => crate::bail!("Invalid storage"),
1844        };
1845        match storage.dtype() {
1846            DType::F32 => {
1847                let slice = storage.as_slice::<f32>()?;
1848                let slice =
1849                    &slice[layout.start_offset()..layout.start_offset() + src_shape.elem_count()];
1850                let mut dst_storage = vec![0f32; dst_shape.elem_count()];
1851                self_storage.matmul_t(
1852                    (dst_shape.elem_count() / n, k, n),
1853                    slice,
1854                    &mut dst_storage,
1855                )?;
1856                Ok((crate::CpuStorage::F32(dst_storage), dst_shape))
1857            }
1858            DType::F16 => {
1859                let slice = storage.as_slice::<f16>()?;
1860                let slice =
1861                    &slice[layout.start_offset()..layout.start_offset() + src_shape.elem_count()];
1862                let mut dst_storage = vec![f16::ZERO; dst_shape.elem_count()];
1863                self_storage.matmul_t_f16(
1864                    (dst_shape.elem_count() / n, k, n),
1865                    slice,
1866                    &mut dst_storage,
1867                )?;
1868                Ok((crate::CpuStorage::F16(dst_storage), dst_shape))
1869            }
1870            _ => crate::bail!("Expected f32/f16"),
1871        }
1872    }
1873
1874    fn metal_fwd(
1875        &self,
1876        storage: &crate::MetalStorage,
1877        layout: &crate::Layout,
1878    ) -> Result<(crate::MetalStorage, Shape)> {
1879        let self_storage = match &self.storage {
1880            QStorage::Metal(metal) => metal,
1881            _ => unreachable!("Cannot call metal matmul on non metal QTensor"),
1882        };
1883        self_storage.fwd(&self.shape, storage, layout)
1884    }
1885
1886    fn cuda_fwd(
1887        &self,
1888        storage: &crate::CudaStorage,
1889        layout: &crate::Layout,
1890    ) -> Result<(crate::CudaStorage, Shape)> {
1891        let self_storage = match &self.storage {
1892            QStorage::Cuda(cuda) => cuda,
1893            _ => unreachable!("Cannot call cuda matmul on non cuda QTensor"),
1894        };
1895        self_storage.fwd(&self.shape, storage, layout)
1896    }
1897}
1898
1899/// Dense (non-quantized) matmul `xs @ w^T` for the `Tensor`/`TensorF16` `QMatMul` variants, where
1900/// the stored weight `w` is `[n, k]` and `xs` is `[.., k]`. On ROCm at decode (a single-row matvec)
1901/// this computes the result as `sum_k(xs[k] * w[n, k])` via pooled broadcast-mul + reduce instead of
1902/// rocBLAS `gemm_ex`. rocBLAS's GEMM dispatch records a vendor-specific PM4 indirect-buffer packet
1903/// that WSL's HSA thunk rejects on hipGraph replay (`VendorSpecificAqlToPm4` assert), so a captured
1904/// decode forward containing one (e.g. the MoE F32 router gate) corrupts/aborts on replay. The
1905/// reduce path uses only ops already exercised under capture (RMSNorm etc.), so it replays cleanly,
1906/// and at M=1 a GEMV-as-reduce is as cheap as the GEMM (it materializes only the `[n, k]` weight).
1907/// Prefill (rows > 1, never graph-captured) keeps the rocBLAS GEMM. Non-ROCm devices are unchanged.
1908fn dense_matmul(xs: &Tensor, w: &Tensor) -> Result<Tensor> {
1909    let k = *w.dims().last().unwrap();
1910    let rows = xs.elem_count() / k;
1911    if rows == 1 && xs.device().is_rocm() {
1912        let n = w.dim(0)?;
1913        #[cfg(feature = "rocm")]
1914        {
1915            // Dense decode GEMV: read the [n,k] weight ONCE (warp/row dot) instead of materializing
1916            // and re-reading the broadcast_mul product. The activation is matched to the weight dtype
1917            // (a [k] cast, negligible); the GEMV stays capture-clean (no rocBLAS).
1918            let d = match xs.device() {
1919                Device::Rocm(d) => d.clone(),
1920                _ => unreachable!(),
1921            };
1922            let xs1 = xs.reshape((k,))?.to_dtype(w.dtype())?.contiguous()?;
1923            let w = w.contiguous()?;
1924            let (wstore, _) = w.storage_and_layout();
1925            let wr = match &*wstore {
1926                crate::Storage::Rocm(r) => r,
1927                _ => crate::bail!("dense_matmul: weight not on rocm"),
1928            };
1929            let (xstore, _) = xs1.storage_and_layout();
1930            let xr = match &*xstore {
1931                crate::Storage::Rocm(r) => r,
1932                _ => crate::bail!("dense_matmul: x not on rocm"),
1933            };
1934            let y = d.dense_gemv(wr, xr, n, k)?;
1935            let mut dims = xs.dims().to_vec();
1936            *dims.last_mut().unwrap() = n;
1937            return crate::tensor::from_storage(
1938                crate::Storage::Rocm(y),
1939                dims,
1940                crate::op::BackpropOp::none(),
1941                false,
1942            )
1943            .to_dtype(xs.dtype());
1944        }
1945        #[cfg(not(feature = "rocm"))]
1946        {
1947            let out = xs.reshape((1, k))?.broadcast_mul(w)?.sum(D::Minus1)?;
1948            let mut dims = xs.dims().to_vec();
1949            *dims.last_mut().unwrap() = n;
1950            return out.reshape(dims);
1951        }
1952    }
1953    let w = match *xs.dims() {
1954        [b1, b2, _, _] => w.broadcast_left((b1, b2))?.t()?,
1955        [bsize, _, _] => w.broadcast_left(bsize)?.t()?,
1956        _ => w.t()?,
1957    };
1958    xs.matmul(&w)
1959}
1960
1961// Prefill row-count gate for the native Vulkan quantized GEMM. The column-per-invocation `mul_mat_q*`
1962// kernel re-reads AND re-decodes the weight ceil(M / MATMUL_Q_MAX_M(8)) times; the dequant path
1963// decodes the weight to f32 once then runs a dense matmul. So the GEMM wins at small/moderate M and
1964// loses once the re-decode dominates. The crossover is dtype-dependent: cheap-decode quants
1965// (Q4_0/Q8_0/Q4_K -- nibble or single-byte unpack) stay ahead through M=128, while the expensive
1966// high-bit super-blocks (Q5_K/Q6_K -- 5/6-bit reconstruction with a qh high-bit gather, compute-bound
1967// at large N) cross earlier and are only safe through M=64. Measured on RADV gfx1151 (Radeon 8060S),
1968// K=4096, N in {4096, 11008}: the chosen rows are the strict-non-regression floor for each family
1969// (worst case ~2.1x at the threshold, up to 25x at M=16). Larger dense prefill keeps the dequant path
1970// until a shared-memory-tiled int8 GEMM (reads + decodes the weight once, removing this gate) lands.
1971#[cfg(feature = "vulkan")]
1972fn vulkan_prefill_gemm_max_rows(dtype: GgmlDType) -> usize {
1973    match dtype {
1974        GgmlDType::Q5K | GgmlDType::Q6K => 64,
1975        _ => 128,
1976    }
1977}
1978
1979impl crate::Module for QMatMul {
1980    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
1981        match self {
1982            #[cfg(feature = "rocm")]
1983            Self::RocmQuant {
1984                qtensor,
1985                wq,
1986                dtype,
1987                n,
1988                k,
1989            } => {
1990                // Device-residency guard: multi-token attention prefill can leave the activation
1991                // off-device (an upstream op leaked to host); recover instead of bailing so prefill
1992                // stays correct. The leak is the bug to fix for speed; this keeps us correct meanwhile.
1993                let xs_recovered = if xs.device().is_rocm() {
1994                    None
1995                } else {
1996                    if std::env::var("HANZO_DBG_PATH").is_ok() {
1997                        eprintln!(
1998                            "[ROCm-RECOVER] activation off-device {:?} dims {:?}",
1999                            xs.device().location(),
2000                            xs.dims()
2001                        );
2002                    }
2003                    Some(xs.to_device(&qtensor.device())?)
2004                };
2005                let xs = xs_recovered.as_ref().unwrap_or(xs);
2006                let rows: usize = xs.elem_count() / *k;
2007                #[cfg(feature = "rocm")]
2008                {
2009                    use std::sync::atomic::{AtomicUsize, Ordering};
2010                    static DBG_DECODE: AtomicUsize = AtomicUsize::new(0);
2011                    static DBG_PREFILL: AtomicUsize = AtomicUsize::new(0);
2012                    static DBG_FALLBACK: AtomicUsize = AtomicUsize::new(0);
2013                    if std::env::var("HANZO_DBG_PATH").is_ok() {
2014                        // Buckets MATCH the real dispatch below: decode = rows==1 wired (the dp4a-vs-
2015                        // scalar A/B is internal to matvec_quant, still native); prefill = rows>1 wired
2016                        // native qmmq_core<WTYPE>; fallback = unwired OR HANZO_QMMQ_FALLBACK dequantize.
2017                        let qt = crate::RocmQuantType::from_ggml(*dtype);
2018                        let decode_wired = qt.is_some();
2019                        let prefill_wired = qt.is_some_and(|qt| qt.qmmq_capable());
2020                        let prefill_fb = std::env::var("HANZO_QMMQ_FALLBACK").is_ok();
2021                        if rows == 1 && decode_wired {
2022                            DBG_DECODE.fetch_add(1, Ordering::Relaxed);
2023                        } else if rows > 1 && prefill_wired && !prefill_fb {
2024                            DBG_PREFILL.fetch_add(1, Ordering::Relaxed);
2025                        } else {
2026                            DBG_FALLBACK.fetch_add(1, Ordering::Relaxed);
2027                        }
2028                        // Print on EVERY rows>1 (prefill is rare vs decode) + every 200 total, so the
2029                        // native-prefill path is always visible the moment it is taken.
2030                        let tot = DBG_DECODE.load(Ordering::Relaxed)
2031                            + DBG_PREFILL.load(Ordering::Relaxed)
2032                            + DBG_FALLBACK.load(Ordering::Relaxed);
2033                        if rows > 1 || tot % 200 == 0 {
2034                            eprintln!(
2035                                "[DBG_PATH] decode={} prefill(native)={} fallback={} (dt={:?} rows={} n={} k={})",
2036                                DBG_DECODE.load(Ordering::Relaxed),
2037                                DBG_PREFILL.load(Ordering::Relaxed),
2038                                DBG_FALLBACK.load(Ordering::Relaxed),
2039                                dtype, rows, *n, *k
2040                            );
2041                        }
2042                    }
2043                }
2044                // Table-driven unified decode: a type is decode-native iff the single
2045                // `qmatvec_core<WTYPE>` has a `decode_block` wired for it (RocmQuantType::from_ggml).
2046                // Q8_0/Q4_0/Q4_K/Q6_K/IQ4_XS/TQ2_0 today; adding a type is one enum row, no kernel.
2047                // The dp4a-vs-scalar A/B for dp4a-capable types (HANZO_Q4K_FALLBACK / HANZO_Q6K_FALLBACK)
2048                // lives entirely inside `matvec_quant` (dp4a_active) -- ONE fallback, one place; it
2049                // switches the decode core, the type stays on the native path either way.
2050                #[cfg(feature = "rocm")]
2051                let unified_qt = crate::RocmQuantType::from_ggml(*dtype);
2052                #[cfg(not(feature = "rocm"))]
2053                let unified_qt: Option<()> = None;
2054                // Native int8-WMMA prefill exists only for `qmmq_capable` types; the decode-only types
2055                // (Q2_K/Q3_K + every IQ*/TQ* codebook/fractional type) dequantize-to-f16 for rows>1 via
2056                // the `else` branch below -- correct, just not WMMA-accelerated. ONE predicate, read here
2057                // and at the two MoE `use_qmmq` sites.
2058                #[cfg(feature = "rocm")]
2059                let qmmq_ok = unified_qt.map(|qt| qt.qmmq_capable()).unwrap_or(false);
2060                #[cfg(not(feature = "rocm"))]
2061                let qmmq_ok = false;
2062                if rows == 1 && unified_qt.is_some() {
2063                    // Decode: weights stay quantized in VRAM; the ONE native on-GPU quant matvec core
2064                    // dequantizes per-block on-the-fly (no dense f16 copy). The matvec consumes
2065                    // bf16/f16 activations directly and returns the same dtype, so the model's working
2066                    // dtype (bf16) is kept end-to-end -- no bf16->f32->f16->bf16 cast detour. Only fall
2067                    // back to an f16 cast for exotic input dtypes. Every wired type (symmetric 8-bit
2068                    // through asymmetric super-block through sub-4-bit ternary) rides the same core.
2069                    // dp4a-capable types accept the F32 residual/norm stream DIRECTLY (q8_1 quantize
2070                    // from f32 + f32-store matvec), so an F32 activation stays F32 end-to-end with no
2071                    // f16 bounce -- this removes the cast_f32_f16-before / cast_f16_f32-after pair that
2072                    // wrapped every decode matvec. Non-dp4a (scalar) types keep the f16 cast.
2073                    #[cfg(feature = "rocm")]
2074                    let keep_f32 = unified_qt.map(|qt| qt.dp4a_active()).unwrap_or(false);
2075                    #[cfg(not(feature = "rocm"))]
2076                    let keep_f32 = false;
2077                    let xs = match xs.dtype() {
2078                        DType::BF16 | DType::F16 => xs.contiguous()?,
2079                        DType::F32 if keep_f32 => xs.contiguous()?,
2080                        _ => xs.to_dtype(DType::F16)?.contiguous()?,
2081                    };
2082                    let d = match xs.device() {
2083                        Device::Rocm(d) => d,
2084                        _ => crate::bail!("RocmQuant input not on rocm"),
2085                    };
2086                    let y = {
2087                        let (store, _) = xs.storage_and_layout();
2088                        let xr = match &*store {
2089                            crate::Storage::Rocm(r) => r,
2090                            _ => crate::bail!("RocmQuant expected rocm storage"),
2091                        };
2092                        #[cfg(feature = "rocm")]
2093                        {
2094                            d.matvec_quant(unified_qt.unwrap(), wq, xr, *n, *k)?
2095                        }
2096                        #[cfg(not(feature = "rocm"))]
2097                        {
2098                            crate::bail!("rocm feature disabled")
2099                        }
2100                    };
2101                    let mut dims = xs.dims().to_vec();
2102                    let last = dims.len() - 1;
2103                    dims[last] = *n;
2104                    Ok(crate::tensor::from_storage(
2105                        crate::Storage::Rocm(y),
2106                        dims,
2107                        crate::op::BackpropOp::none(),
2108                        false,
2109                    ))
2110                } else if let Some(qt) =
2111                    unified_qt.filter(|_| qmmq_ok && std::env::var("HANZO_QMMQ_FALLBACK").is_err())
2112                {
2113                    // Prefill (rows>1): native int8 WMMA gemm through the ONE unified core
2114                    // (`qmmq_core<WTYPE>` in quant.hip). Weights stay quantized in VRAM (no resident
2115                    // dense f16, which would slow the memory-bound decode) and the MAC runs on the
2116                    // RDNA3 int8 matrix cores instead of rocBLAS. The SAME core covers the whole wired
2117                    // spread: Q8_0/Q4_0 (symmetric, proven), Q4_K (asymmetric -- min bias via the
2118                    // q8_1 block-sum), and the symmetric super-block / IQ / ternary types (Q6_K,
2119                    // IQ4_XS, TQ2_0). Selecting the type is one `RocmQuantType` row + the in-kernel
2120                    // decode; there is NO per-quant prefill kernel. HANZO_QMMQ_FALLBACK=1 forces the
2121                    // dequant-f16 matmul below (the prefill before/after A/B benchmark lever).
2122                    let xs = xs.to_dtype(DType::F16)?.contiguous()?;
2123                    let d = match xs.device() {
2124                        Device::Rocm(d) => d,
2125                        _ => crate::bail!("RocmQuant input not on rocm"),
2126                    };
2127                    let m = xs.elem_count() / *k;
2128                    let y = {
2129                        let (store, _) = xs.storage_and_layout();
2130                        let xr = match &*store {
2131                            crate::Storage::Rocm(r) => r,
2132                            _ => crate::bail!("RocmQuant expected rocm storage"),
2133                        };
2134                        #[cfg(feature = "rocm")]
2135                        {
2136                            d.qmmq_quant(qt, xr, wq, m, *n, *k)?
2137                        }
2138                        #[cfg(not(feature = "rocm"))]
2139                        {
2140                            let _ = qt;
2141                            crate::bail!("rocm feature disabled")
2142                        }
2143                    };
2144                    let mut dims = xs.dims().to_vec();
2145                    let last = dims.len() - 1;
2146                    dims[last] = *n;
2147                    Ok(crate::tensor::from_storage(
2148                        crate::Storage::Rocm(y),
2149                        dims,
2150                        crate::op::BackpropOp::none(),
2151                        false,
2152                    ))
2153                } else {
2154                    // Unwired-type / forced-fallback prefill: dequantize to a temporary f16 weight
2155                    // (freed after; a persistent f16 copy would slow the memory-bound decode). Only
2156                    // reached for quants with no `qmmq_core<WTYPE>` wired (e.g. Q5_K/MXFP4) or when
2157                    // HANZO_QMMQ_FALLBACK forces it for the prefill A/B measurement.
2158                    let w = qtensor.dequantize_f16(&xs.device())?;
2159                    let w = match *xs.dims() {
2160                        [b1, b2, _, _] => w.broadcast_left((b1, b2))?.t()?,
2161                        [bsize, _, _] => w.broadcast_left(bsize)?.t()?,
2162                        _ => w.t()?,
2163                    };
2164                    xs.to_dtype(DType::F16)?.matmul(&w)
2165                }
2166            }
2167            #[cfg(feature = "vulkan")]
2168            Self::VulkanQuant {
2169                qtensor,
2170                wq,
2171                dtype,
2172                n,
2173                k,
2174            } => {
2175                let rows: usize = xs.elem_count() / *k;
2176                if rows == 1 {
2177                    // Decode: weights stay quantized in VRAM; the matching native-GGML quant matvec
2178                    // runs straight out of the block format (no dequant, no copy).
2179                    let xs = xs.contiguous()?;
2180                    let d = match xs.device() {
2181                        Device::Vulkan(d) => d,
2182                        _ => crate::bail!("VulkanQuant input not on vulkan"),
2183                    };
2184                    let y = {
2185                        let (store, _) = xs.storage_and_layout();
2186                        let xv = match &*store {
2187                            crate::Storage::Vulkan(v) => v,
2188                            _ => crate::bail!("VulkanQuant expected vulkan storage"),
2189                        };
2190                        match dtype {
2191                            GgmlDType::Q4_0 => d.matvec_q4_0_gpu(wq, xv, *n, *k)?,
2192                            // Q8_0 rides the 9-u32 repacked layout (mul_mat_vec_q8), the SAME blocks
2193                            // the prefill GEMM (mul_mat_q8) reads -- one layout for decode + prefill.
2194                            GgmlDType::Q8_0 => d.matvec_q8_gpu(wq, xv, *n, *k)?,
2195                            GgmlDType::Q4K => d.matvec_q4k_gpu(wq, xv, *n, *k)?,
2196                            GgmlDType::Q5K => d.matvec_q5k_gpu(wq, xv, *n, *k)?,
2197                            GgmlDType::Q6K => d.matvec_q6k_gpu(wq, xv, *n, *k)?,
2198                            GgmlDType::Q2K => d.matvec_q2k_gpu(wq, xv, *n, *k)?,
2199                            GgmlDType::Q3K => d.matvec_q3k_gpu(wq, xv, *n, *k)?,
2200                            GgmlDType::IQ4_XS => d.matvec_iq4xs_gpu(wq, xv, *n, *k)?,
2201                            GgmlDType::IQ4_NL => d.matvec_iq4nl_gpu(wq, xv, *n, *k)?,
2202                            GgmlDType::IQ2_XXS => d.matvec_iq2xxs_gpu(wq, xv, *n, *k)?,
2203                            GgmlDType::IQ2_XS => d.matvec_iq2xs_gpu(wq, xv, *n, *k)?,
2204                            GgmlDType::IQ1_M => d.matvec_iq1m_gpu(wq, xv, *n, *k)?,
2205                            GgmlDType::IQ1_S => d.matvec_iq1s_gpu(wq, xv, *n, *k)?,
2206                            GgmlDType::IQ3_S => d.matvec_iq3s_gpu(wq, xv, *n, *k)?,
2207                            GgmlDType::IQ3_XXS => d.matvec_iq3xxs_gpu(wq, xv, *n, *k)?,
2208                            GgmlDType::IQ2_S => d.matvec_iq2s_gpu(wq, xv, *n, *k)?,
2209                            GgmlDType::TQ2_0 => d.matvec_tq2_0_gpu(wq, xv, *n, *k)?,
2210                            other => crate::bail!("VulkanQuant: no native matvec for {other:?}"),
2211                        }
2212                    };
2213                    let mut dims = xs.dims().to_vec();
2214                    let last = dims.len() - 1;
2215                    dims[last] = *n;
2216                    Ok(crate::tensor::from_storage(
2217                        crate::Storage::Vulkan(y),
2218                        dims,
2219                        crate::op::BackpropOp::none(),
2220                        false,
2221                    ))
2222                } else if rows <= vulkan_prefill_gemm_max_rows(*dtype) {
2223                    // Prefill (small/moderate M): native quantized GEMM. Weights stay quantized in
2224                    // VRAM and each weight block is decoded ONCE per output column then reused across
2225                    // a tile of up to MATMUL_Q_MAX_M(=8) rows -- so the weight is re-read+re-decoded
2226                    // ceil(M/8) times, vs the dequant path's one-time f32 materialization. The GEMM
2227                    // therefore wins decisively while M is small (short / chunked prefill, batched
2228                    // decode) and would lose at large M, which the dtype-aware `rows` gate routes to
2229                    // the dequant path below. Same block layout + decode as the decode matvec above;
2230                    // one matmul_q*_gpu per native dtype. Leading batch dims flatten into M.
2231                    let m = rows;
2232                    let xs = xs.contiguous()?;
2233                    let d = match xs.device() {
2234                        Device::Vulkan(d) => d,
2235                        _ => crate::bail!("VulkanQuant input not on vulkan"),
2236                    };
2237                    let y = {
2238                        let (store, _) = xs.storage_and_layout();
2239                        let xv = match &*store {
2240                            crate::Storage::Vulkan(v) => v,
2241                            _ => crate::bail!("VulkanQuant expected vulkan storage"),
2242                        };
2243                        match dtype {
2244                            GgmlDType::Q4_0 => d.matmul_q4_0_gpu(wq, xv, m, *n, *k)?,
2245                            GgmlDType::Q8_0 => d.matmul_q8_gpu(wq, xv, m, *n, *k)?,
2246                            GgmlDType::Q4K => d.matmul_q4k_gpu(wq, xv, m, *n, *k)?,
2247                            GgmlDType::Q5K => d.matmul_q5k_gpu(wq, xv, m, *n, *k)?,
2248                            GgmlDType::Q6K => d.matmul_q6k_gpu(wq, xv, m, *n, *k)?,
2249                            other => crate::bail!("VulkanQuant: no native matmul for {other:?}"),
2250                        }
2251                    };
2252                    let mut dims = xs.dims().to_vec();
2253                    let last = dims.len() - 1;
2254                    dims[last] = *n;
2255                    Ok(crate::tensor::from_storage(
2256                        crate::Storage::Vulkan(y),
2257                        dims,
2258                        crate::op::BackpropOp::none(),
2259                        false,
2260                    ))
2261                } else {
2262                    // Large dense prefill (M > the crossover): the column-per-invocation GEMM would
2263                    // re-read the weight ceil(M/8) times and lose to materializing the f32 weight once
2264                    // and running a dense matmul. Keep the dequant path here -- a strict non-regression
2265                    // until a shared-memory-tiled int8 GEMM (reads the weight once) removes the gate.
2266                    let w = qtensor.dequantize(&xs.device())?;
2267                    let w = match *xs.dims() {
2268                        [b1, b2, _, _] => w.broadcast_left((b1, b2))?.t()?,
2269                        [bsize, _, _] => w.broadcast_left(bsize)?.t()?,
2270                        _ => w.t()?,
2271                    };
2272                    xs.matmul(&w)
2273                }
2274            }
2275            #[cfg(feature = "wgpu")]
2276            Self::WgpuQuant {
2277                qtensor,
2278                wq,
2279                dtype,
2280                n,
2281                k,
2282            } => {
2283                let rows: usize = xs.elem_count() / *k;
2284                if rows == 1 {
2285                    // Decode: weights stay quantized in VRAM; the matching native-GGML quant matvec
2286                    // WGSL kernel runs straight out of the block format (no dequant, no copy).
2287                    let xs = xs.contiguous()?;
2288                    let d = match xs.device() {
2289                        Device::Wgpu(d) => d,
2290                        _ => crate::bail!("WgpuQuant input not on wgpu"),
2291                    };
2292                    let y = {
2293                        let (store, _) = xs.storage_and_layout();
2294                        let xv = match &*store {
2295                            crate::Storage::Wgpu(v) => v,
2296                            _ => crate::bail!("WgpuQuant expected wgpu storage"),
2297                        };
2298                        match dtype {
2299                            GgmlDType::Q4_0 => d.matvec_q4_0_gpu(wq, xv, *n, *k)?,
2300                            GgmlDType::Q8_0 => d.matvec_q8_0_gpu(wq, xv, *n, *k)?,
2301                            GgmlDType::Q4K => d.matvec_q4k_gpu(wq, xv, *n, *k)?,
2302                            other => crate::bail!("WgpuQuant: no native matvec for {other:?}"),
2303                        }
2304                    };
2305                    let mut dims = xs.dims().to_vec();
2306                    let last = dims.len() - 1;
2307                    dims[last] = *n;
2308                    Ok(crate::tensor::from_storage(
2309                        crate::Storage::Wgpu(y),
2310                        dims,
2311                        crate::op::BackpropOp::none(),
2312                        false,
2313                    ))
2314                } else {
2315                    // Prefill: dequantize to a temporary f32 weight (reuses the NT matmul path).
2316                    let w = qtensor.dequantize(&xs.device())?;
2317                    let w = match *xs.dims() {
2318                        [b1, b2, _, _] => w.broadcast_left((b1, b2))?.t()?,
2319                        [bsize, _, _] => w.broadcast_left(bsize)?.t()?,
2320                        _ => w.t()?,
2321                    };
2322                    xs.matmul(&w)
2323                }
2324            }
2325            Self::QTensor(t) => xs.apply_op1_no_bwd(t.as_ref()),
2326            Self::Tensor(w) => dense_matmul(xs, w),
2327            Self::TensorF16(w) => {
2328                let in_dtype = xs.dtype();
2329                dense_matmul(&xs.to_dtype(DType::F16)?, w)?.to_dtype(in_dtype)
2330            }
2331        }
2332    }
2333}