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;
7use std::sync::Arc;
8
9#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
10pub mod avx;
11pub mod dsv4_qat;
12mod dummy_cuda;
13mod dummy_metal;
14pub mod expert_stream;
15pub mod ggml_file;
16pub mod gguf_file;
17pub mod imatrix_file;
18mod iq_grids;
19pub mod iq_quants;
20pub mod k_quants;
21#[cfg(feature = "metal")]
22pub mod metal;
23pub mod repack;
24#[cfg(target_arch = "x86_64")]
25pub(crate) mod repack_x86;
26#[cfg(not(target_arch = "wasm32"))]
27pub mod tokenizer;
28#[cfg(not(feature = "metal"))]
29mod metal {
30    pub use super::dummy_metal::*;
31}
32#[cfg(feature = "cuda")]
33pub mod cuda;
34#[cfg(feature = "cuda")]
35pub mod fast_mmq;
36#[cfg(feature = "cuda")]
37pub mod fast_mmvq;
38#[cfg(not(feature = "cuda"))]
39mod cuda {
40    pub use super::dummy_cuda::*;
41}
42
43#[cfg(any(
44    target_arch = "aarch64",
45    all(target_arch = "arm", target_feature = "neon")
46))]
47pub mod neon;
48#[cfg(target_feature = "simd128")]
49pub mod simd128;
50pub mod utils;
51// Declarative GGUF quant-type formatter (Cut 2). Additive: defines `quant_format!` and a
52// `#[cfg(test)]` equivalence proof; does not alter any existing type or wiring.
53pub mod quant_format;
54use half::{bf16, f16};
55
56pub use k_quants::GgmlType;
57
58// Borrows `data` (does not consume it) so the returned slice stays valid for the caller's lifetime.
59// Taking `Cow` by value here was a use-after-free: the Cow dropped at return, dangling the slice for
60// Cow::Owned inputs (segfault on large quantized tensors; mmap'd Cow::Borrowed happened to survive).
61fn as_t_slice<T>(data: &[u8]) -> &[T] {
62    let size = std::mem::size_of::<T>();
63    assert_eq!(
64        data.len() % size,
65        0,
66        "Data length must be a multiple of T's size"
67    );
68    let ptr = data.as_ptr();
69    assert_eq!(
70        (ptr as usize) % std::mem::align_of::<T>(),
71        0,
72        "Data pointer must be aligned to T's alignment"
73    );
74    unsafe { std::slice::from_raw_parts(ptr as *const T, data.len() / size) }
75}
76
77// The GPU-resident copies of this tensor's expert bank. A resident bank is a value DERIVED from
78// these exact quantized bytes, so it is owned by the tensor that owns the bytes and dies with them.
79// One slot per backend; each fills at most once (first routed token) and is then shared by every
80// later call through the `Arc<QTensor>` all consumers already hold.
81#[derive(Default)]
82struct ResidentBanks {
83    #[cfg(feature = "rocm")]
84    rocm: std::sync::OnceLock<std::sync::Arc<crate::RocmStorage>>,
85    #[cfg(feature = "vulkan")]
86    vulkan: std::sync::OnceLock<std::sync::Arc<crate::VulkanStorage>>,
87    #[cfg(feature = "vulkan")]
88    vulkan_split: std::sync::OnceLock<std::sync::Arc<crate::vulkan::MoeBankSplit>>,
89    #[cfg(feature = "wgpu")]
90    wgpu: std::sync::OnceLock<std::sync::Arc<crate::WgpuStorage>>,
91}
92
93pub struct QTensor {
94    storage: QStorage,
95    shape: Shape,
96    // Every field of ResidentBanks is gated on rocm/vulkan/wgpu, so with none of
97    // them enabled it is an empty struct and this field is genuinely never read —
98    // which is what clippy reports on a default-feature build. It IS read on any
99    // accelerator build (see the cache_or_upload calls below), so the allowance is
100    // scoped to exactly the configuration where the field is dead rather than
101    // silencing the lint everywhere.
102    #[cfg_attr(
103        not(any(feature = "rocm", feature = "vulkan", feature = "wgpu")),
104        allow(dead_code)
105    )]
106    banks: ResidentBanks,
107    #[allow(dead_code)]
108    repacked_qs: repack::PackedCache,
109}
110
111impl Device {
112    fn qzeros(&self, elem_count: usize, dtype: GgmlDType) -> Result<QStorage> {
113        match self {
114            Device::Cpu => {
115                let storage = dtype.cpu_zeros(elem_count);
116                Ok(QStorage::Cpu(storage))
117            }
118            Device::Metal(metal) => {
119                let storage = metal::QMetalStorage::zeros(metal, elem_count, dtype)?;
120                Ok(QStorage::Metal(storage))
121            }
122            Device::Cuda(cuda) => {
123                let storage = cuda::QCudaStorage::zeros(cuda, elem_count, dtype)?;
124                Ok(QStorage::Cuda(storage))
125            }
126            #[cfg(feature = "rocm")]
127            Device::Rocm(d) => {
128                // Mirror Vulkan: keep quantized blocks in a CPU box + the device; QMatMul
129                // dequantizes them to a dense ROCm tensor on use.
130                let storage = dtype.cpu_zeros(elem_count);
131                Ok(QStorage::Rocm(storage, d.clone()))
132            }
133            #[cfg(feature = "vulkan")]
134            Device::Vulkan(d) => {
135                // Keep quantized blocks in a CPU box + the device (same as from_data); QMatMul's
136                // VulkanQuant path uploads/dequantizes them to the GPU on use.
137                let storage = dtype.cpu_zeros(elem_count);
138                Ok(QStorage::Vulkan(storage, d.clone()))
139            }
140            #[cfg(feature = "wgpu")]
141            Device::Wgpu(d) => {
142                // Same as Vulkan: keep the quantized blocks in a CPU box + the device; QMatMul's
143                // WgpuQuant path uploads them to the GPU on use.
144                let storage = dtype.cpu_zeros(elem_count);
145                Ok(QStorage::Wgpu(storage, d.clone()))
146            }
147        }
148    }
149}
150
151pub enum QStorage {
152    Cpu(Box<dyn QuantizedType>),
153    Metal(metal::QMetalStorage),
154    Cuda(cuda::QCudaStorage),
155    // ROCm mirrors the Vulkan path: quantized blocks held in a CPU box + the device; QMatMul
156    // dequantizes them to a dense f32 ROCm tensor on demand (rocBLAS matmul). A native HIP quant
157    // matmul is the bandwidth-win follow-up.
158    #[cfg(feature = "rocm")]
159    Rocm(Box<dyn QuantizedType>, crate::RocmDevice),
160    // Vulkan keeps the quantized blocks in a CPU box and dequantizes to an f32 Vulkan tensor on
161    // demand (QMatMul forces dequantize for Vulkan). Lets GGUF-quantized models run on the GPU;
162    // a direct quantized matmul (the Q8 kernel) is the bandwidth-win follow-up.
163    #[cfg(feature = "vulkan")]
164    Vulkan(Box<dyn QuantizedType>, crate::VulkanDevice),
165    // wgpu mirror of the Vulkan path: quantized blocks held in a CPU box + the device. QMatMul's
166    // WgpuQuant path reads the GGML bytes straight in the native quant matvec kernel (decode), or
167    // dequantizes to an f32 wgpu tensor on demand.
168    #[cfg(feature = "wgpu")]
169    Wgpu(Box<dyn QuantizedType>, crate::WgpuDevice),
170    // Disk-streaming MoE expert bank: the stacked [n_experts, n, k] quantized weight lives on NVMe,
171    // not resident. Only `indexed_moe_forward` consumes it -- it fetches one expert's [n, k] slice
172    // through the pin/LRU cache per token (host, CPU matmul). The low-memory mode for huge MoEs.
173    Stream(Arc<expert_stream::ExpertStreamBank>),
174}
175
176impl QStorage {
177    pub fn from_data(data: Cow<'_, [u8]>, device: &Device, dtype: GgmlDType) -> Result<Self> {
178        let data: &[u8] = &data;
179        match device {
180            Device::Cpu => Ok(Self::Cpu(dtype.from_data(Cow::Borrowed(data)))),
181            Device::Metal(d) => match dtype {
182                GgmlDType::F32 => metal::load_quantized(d, as_t_slice::<f32>(&data)),
183                GgmlDType::F16 => metal::load_quantized(d, as_t_slice::<f16>(&data)),
184                GgmlDType::Q4_0 => metal::load_quantized(d, as_t_slice::<BlockQ4_0>(&data)),
185                GgmlDType::Q4_1 => metal::load_quantized(d, as_t_slice::<BlockQ4_1>(&data)),
186                GgmlDType::Q5_0 => metal::load_quantized(d, as_t_slice::<BlockQ5_0>(&data)),
187                GgmlDType::Q5_1 => metal::load_quantized(d, as_t_slice::<BlockQ5_1>(&data)),
188                GgmlDType::Q8_0 => metal::load_quantized(d, as_t_slice::<BlockQ8_0>(&data)),
189                GgmlDType::Q8_1 => metal::load_quantized(d, as_t_slice::<BlockQ8_1>(&data)),
190                GgmlDType::Q2K => metal::load_quantized(d, as_t_slice::<BlockQ2K>(&data)),
191                GgmlDType::Q3K => metal::load_quantized(d, as_t_slice::<BlockQ3K>(&data)),
192                GgmlDType::Q4K => metal::load_quantized(d, as_t_slice::<BlockQ4K>(&data)),
193                GgmlDType::Q5K => metal::load_quantized(d, as_t_slice::<BlockQ5K>(&data)),
194                GgmlDType::Q6K => metal::load_quantized(d, as_t_slice::<BlockQ6K>(&data)),
195                GgmlDType::Q8K => metal::load_quantized(d, as_t_slice::<BlockQ8K>(&data)),
196                GgmlDType::IQ4_NL => metal::load_quantized(d, as_t_slice::<BlockIQ4nl>(&data)),
197                GgmlDType::IQ4_XS => metal::load_quantized(d, as_t_slice::<BlockIQ4xs>(&data)),
198                GgmlDType::MXFP4 => metal::load_quantized(d, as_t_slice::<BlockMXFP4>(&data)),
199                GgmlDType::BF16 => metal::load_quantized(d, as_t_slice::<bf16>(&data)),
200                GgmlDType::I32 => metal::load_quantized(d, as_t_slice::<i32>(&data)),
201                // i-quant codebook family: the GGML blocks upload to the Metal buffer byte-for-byte
202                // like any other quant, and the native MSL matvec/matmul/mul_mv_id kernels
203                // (kernel_mul_mv_iq*_f32 etc.) read the block format directly -- no CPU dequant on the
204                // hot path. QMetalStorage::dequantize (CPU codebook decode) backs ISQ / dequant-to-f32.
205                // Exhaustive on purpose: a future GgmlDType fails-closed at compile rather than at load.
206                GgmlDType::IQ2_XXS => metal::load_quantized(d, as_t_slice::<BlockIQ2xxs>(&data)),
207                GgmlDType::IQ2_XS => metal::load_quantized(d, as_t_slice::<BlockIQ2xs>(&data)),
208                GgmlDType::IQ2_S => metal::load_quantized(d, as_t_slice::<BlockIQ2s>(&data)),
209                GgmlDType::IQ3_XXS => metal::load_quantized(d, as_t_slice::<BlockIQ3xxs>(&data)),
210                GgmlDType::IQ3_S => metal::load_quantized(d, as_t_slice::<BlockIQ3s>(&data)),
211                GgmlDType::IQ1_S => metal::load_quantized(d, as_t_slice::<BlockIQ1s>(&data)),
212                GgmlDType::IQ1_M => metal::load_quantized(d, as_t_slice::<BlockIQ1m>(&data)),
213                // Ternary / NVFP4 have no native Metal kernel yet (CPU-decode only).
214                other => crate::bail!("{other:?} is not supported on the Metal backend"),
215            },
216            Device::Cuda(d) => match dtype {
217                GgmlDType::F32 => cuda::load_quantized(d, as_t_slice::<f32>(&data)),
218                GgmlDType::F16 => cuda::load_quantized(d, as_t_slice::<f16>(&data)),
219                GgmlDType::Q4_0 => cuda::load_quantized(d, as_t_slice::<BlockQ4_0>(&data)),
220                GgmlDType::Q4_1 => cuda::load_quantized(d, as_t_slice::<BlockQ4_1>(&data)),
221                GgmlDType::Q5_0 => cuda::load_quantized(d, as_t_slice::<BlockQ5_0>(&data)),
222                GgmlDType::Q5_1 => cuda::load_quantized(d, as_t_slice::<BlockQ5_1>(&data)),
223                GgmlDType::Q8_0 => cuda::load_quantized(d, as_t_slice::<BlockQ8_0>(&data)),
224                GgmlDType::Q8_1 => cuda::load_quantized(d, as_t_slice::<BlockQ8_1>(&data)),
225                GgmlDType::Q2K => cuda::load_quantized(d, as_t_slice::<BlockQ2K>(&data)),
226                GgmlDType::Q3K => cuda::load_quantized(d, as_t_slice::<BlockQ3K>(&data)),
227                GgmlDType::Q4K => cuda::load_quantized(d, as_t_slice::<BlockQ4K>(&data)),
228                GgmlDType::Q5K => cuda::load_quantized(d, as_t_slice::<BlockQ5K>(&data)),
229                GgmlDType::Q6K => cuda::load_quantized(d, as_t_slice::<BlockQ6K>(&data)),
230                GgmlDType::Q8K => cuda::load_quantized(d, as_t_slice::<BlockQ8K>(&data)),
231                GgmlDType::IQ4_NL => cuda::load_quantized(d, as_t_slice::<BlockIQ4nl>(&data)),
232                GgmlDType::IQ4_XS => cuda::load_quantized(d, as_t_slice::<BlockIQ4xs>(&data)),
233                GgmlDType::MXFP4 => cuda::load_quantized(d, as_t_slice::<BlockMXFP4>(&data)),
234                GgmlDType::BF16 => cuda::load_quantized(d, as_t_slice::<bf16>(&data)),
235                GgmlDType::I32 => cuda::load_quantized(d, as_t_slice::<i32>(&data)),
236                // IQ / ternary / 1-bit / NVFP4 codec types: no native on-GPU quant-matmul kernel, but the
237                // GGML blocks upload to VRAM byte-for-byte like any other quant. QCudaStorage::fwd then
238                // dequantizes them to f32 (CPU codebook decode + upload) for a dense matmul -- see
239                // `has_native_q8_1_matmul`. So an i-quant GGUF that used to bail here now LOADS and DECODES
240                // on CUDA, exact w.r.t. the CPU reference; a native mmvq kernel is the bandwidth follow-up.
241                // Exhaustive on purpose: a future GgmlDType must be wired here (fail-closed at compile) rather
242                // than silently bailing at load.
243                GgmlDType::IQ2_XXS => cuda::load_quantized(d, as_t_slice::<BlockIQ2xxs>(&data)),
244                GgmlDType::IQ2_XS => cuda::load_quantized(d, as_t_slice::<BlockIQ2xs>(&data)),
245                GgmlDType::IQ2_S => cuda::load_quantized(d, as_t_slice::<BlockIQ2s>(&data)),
246                GgmlDType::IQ3_XXS => cuda::load_quantized(d, as_t_slice::<BlockIQ3xxs>(&data)),
247                GgmlDType::IQ3_S => cuda::load_quantized(d, as_t_slice::<BlockIQ3s>(&data)),
248                GgmlDType::IQ1_S => cuda::load_quantized(d, as_t_slice::<BlockIQ1s>(&data)),
249                GgmlDType::IQ1_M => cuda::load_quantized(d, as_t_slice::<BlockIQ1m>(&data)),
250                GgmlDType::TQ1_0 => cuda::load_quantized(d, as_t_slice::<BlockTQ1_0>(&data)),
251                GgmlDType::TQ2_0 => cuda::load_quantized(d, as_t_slice::<BlockTQ2_0>(&data)),
252                GgmlDType::NVFP4 => cuda::load_quantized(d, as_t_slice::<BlockNVFP4>(&data)),
253                GgmlDType::Q1_0 => cuda::load_quantized(d, as_t_slice::<BlockQ1_0>(&data)),
254                GgmlDType::ROCMFP4 => cuda::load_quantized(d, as_t_slice::<BlockROCMFP4>(&data)),
255                GgmlDType::ROCMFP4_FAST => {
256                    cuda::load_quantized(d, as_t_slice::<BlockROCMFP4Fast>(&data))
257                }
258            },
259            #[cfg(feature = "rocm")]
260            Device::Rocm(d) => Ok(Self::Rocm(dtype.from_data(Cow::Borrowed(data)), d.clone())),
261            #[cfg(feature = "vulkan")]
262            Device::Vulkan(d) => Ok(Self::Vulkan(
263                dtype.from_data(Cow::Borrowed(data)),
264                d.clone(),
265            )),
266            #[cfg(feature = "wgpu")]
267            Device::Wgpu(d) => Ok(Self::Wgpu(dtype.from_data(Cow::Borrowed(data)), d.clone())),
268        }
269    }
270
271    fn block_size(&self) -> usize {
272        match self {
273            QStorage::Cpu(storage) => storage.block_size(),
274            QStorage::Metal(storage) => storage.dtype().block_size(),
275            QStorage::Cuda(storage) => storage.dtype().block_size(),
276            #[cfg(feature = "rocm")]
277            QStorage::Rocm(storage, _) => storage.block_size(),
278            #[cfg(feature = "vulkan")]
279            QStorage::Vulkan(storage, _) => storage.block_size(),
280            #[cfg(feature = "wgpu")]
281            QStorage::Wgpu(storage, _) => storage.block_size(),
282            QStorage::Stream(bank) => bank.dtype().block_size(),
283        }
284    }
285
286    fn dtype(&self) -> GgmlDType {
287        match self {
288            QStorage::Cpu(storage) => storage.dtype(),
289            QStorage::Metal(storage) => storage.dtype(),
290            QStorage::Cuda(storage) => storage.dtype(),
291            #[cfg(feature = "rocm")]
292            QStorage::Rocm(storage, _) => storage.dtype(),
293            #[cfg(feature = "vulkan")]
294            QStorage::Vulkan(storage, _) => storage.dtype(),
295            #[cfg(feature = "wgpu")]
296            QStorage::Wgpu(storage, _) => storage.dtype(),
297            QStorage::Stream(bank) => bank.dtype(),
298        }
299    }
300
301    fn device(&self) -> Device {
302        match self {
303            QStorage::Cpu(_storage) => Device::Cpu,
304            QStorage::Metal(storage) => Device::Metal(storage.device().clone()),
305            QStorage::Cuda(storage) => Device::Cuda(storage.device().clone()),
306            #[cfg(feature = "rocm")]
307            QStorage::Rocm(_storage, device) => Device::Rocm(device.clone()),
308            #[cfg(feature = "vulkan")]
309            QStorage::Vulkan(_storage, device) => Device::Vulkan(device.clone()),
310            #[cfg(feature = "wgpu")]
311            QStorage::Wgpu(_storage, device) => Device::Wgpu(device.clone()),
312            QStorage::Stream(_) => Device::Cpu,
313        }
314    }
315
316    fn size_in_bytes(&self) -> usize {
317        match self {
318            QStorage::Cpu(storage) => storage.storage_size_in_bytes(),
319            QStorage::Metal(storage) => storage.storage_size_in_bytes(),
320            QStorage::Cuda(storage) => storage.storage_size_in_bytes(),
321            #[cfg(feature = "rocm")]
322            QStorage::Rocm(storage, _) => storage.storage_size_in_bytes(),
323            #[cfg(feature = "vulkan")]
324            QStorage::Vulkan(storage, _) => storage.storage_size_in_bytes(),
325            #[cfg(feature = "wgpu")]
326            QStorage::Wgpu(storage, _) => storage.storage_size_in_bytes(),
327            QStorage::Stream(bank) => bank.logical_bytes(),
328        }
329    }
330
331    fn quantize(&mut self, src: &Storage) -> Result<()> {
332        match (self, src) {
333            (QStorage::Cpu(storage), Storage::Cpu(src)) => {
334                storage.from_float(src.as_slice::<f32>()?);
335            }
336            (QStorage::Metal(storage), Storage::Metal(src)) => storage.quantize(src)?,
337            (QStorage::Cuda(storage), Storage::Cuda(src)) => storage.quantize(src)?,
338            _ => crate::bail!("Invalid quantize storage locations do not match"),
339        }
340        Ok(())
341    }
342
343    fn quantize_imatrix(
344        &mut self,
345        src: &Storage,
346        imatrix_weights: &[f32],
347        n_per_row: usize,
348    ) -> Result<()> {
349        match (self, src) {
350            (QStorage::Cpu(storage), Storage::Cpu(src)) => {
351                storage.from_float_imatrix(src.as_slice::<f32>()?, imatrix_weights, n_per_row);
352            }
353            (QStorage::Metal(storage), Storage::Metal(src)) => {
354                storage.quantize_imatrix(src, imatrix_weights, n_per_row)?
355            }
356            (QStorage::Cuda(storage), Storage::Cuda(src)) => {
357                storage.quantize_imatrix(src, imatrix_weights, n_per_row)?
358            }
359            _ => crate::bail!("Invalid quantize storage locations do not match"),
360        }
361        Ok(())
362    }
363
364    fn quantize_onto(&mut self, src: &Storage) -> Result<()> {
365        match (self, src) {
366            (QStorage::Cpu(storage), Storage::Cpu(src)) => {
367                storage.from_float(src.as_slice::<f32>()?);
368            }
369            (QStorage::Metal(storage), Storage::Cpu(src)) => storage.quantize_onto(src)?,
370            (QStorage::Cuda(storage), Storage::Cpu(src)) => storage.quantize_onto(src)?,
371            _ => crate::bail!("Invalid quantize source storage locations: not on cpu"),
372        }
373        Ok(())
374    }
375
376    fn quantize_imatrix_onto(
377        &mut self,
378        src: &Storage,
379        imatrix_weights: &[f32],
380        n_per_row: usize,
381    ) -> Result<()> {
382        match (self, src) {
383            (QStorage::Cpu(storage), Storage::Cpu(src)) => {
384                storage.from_float_imatrix(src.as_slice::<f32>()?, imatrix_weights, n_per_row);
385            }
386            (QStorage::Metal(storage), Storage::Cpu(src)) => {
387                storage.quantize_imatrix_onto(src, imatrix_weights, n_per_row)?
388            }
389            (QStorage::Cuda(storage), Storage::Cpu(src)) => {
390                storage.quantize_imatrix_onto(src, imatrix_weights, n_per_row)?
391            }
392            _ => crate::bail!("Invalid quantize storage locations do not match"),
393        }
394        Ok(())
395    }
396
397    fn dequantize(&self, elem_count: usize) -> Result<Storage> {
398        match self {
399            QStorage::Cpu(storage) => Ok(Storage::Cpu(storage.dequantize(elem_count)?)),
400            QStorage::Metal(storage) => Ok(Storage::Metal(storage.dequantize(elem_count)?)),
401            QStorage::Cuda(storage) => Ok(Storage::Cuda(storage.dequantize(elem_count)?)),
402            #[cfg(feature = "rocm")]
403            QStorage::Rocm(storage, device) => {
404                // Dequantize on the CPU, then upload the dense f32 weights to the ROCm device.
405                use crate::backend::BackendDevice;
406                let cpu = storage.dequantize(elem_count)?;
407                Ok(Storage::Rocm(device.storage_from_cpu_storage(&cpu)?))
408            }
409            #[cfg(feature = "vulkan")]
410            QStorage::Vulkan(storage, device) => {
411                // Dequantize on the CPU, then upload the f32 weights to the GPU.
412                let cpu = storage.dequantize(elem_count)?;
413                Ok(Storage::Vulkan(device.upload_f32(cpu.as_slice::<f32>()?)?))
414            }
415            #[cfg(feature = "wgpu")]
416            QStorage::Wgpu(storage, device) => {
417                // Dequantize on the CPU, then upload the f32 weights to the GPU.
418                let cpu = storage.dequantize(elem_count)?;
419                Ok(Storage::Wgpu(device.upload_f32(cpu.as_slice::<f32>()?)?))
420            }
421            QStorage::Stream(_) => {
422                crate::bail!("streaming expert bank has no whole-tensor dequantize; consume it via indexed_moe_forward")
423            }
424        }
425    }
426
427    fn data(&self) -> Result<Cow<'_, [u8]>> {
428        match self {
429            QStorage::Cpu(storage) => {
430                let data_ptr = storage.as_ptr();
431                let size_in_bytes = storage.storage_size_in_bytes();
432                let data = unsafe { std::slice::from_raw_parts(data_ptr, size_in_bytes) };
433                Ok(Cow::from(data))
434            }
435            QStorage::Cuda(storage) => Ok(Cow::from(storage.data()?)),
436            QStorage::Metal(storage) => Ok(Cow::from(storage.data()?)),
437            #[cfg(feature = "rocm")]
438            QStorage::Rocm(storage, _) => {
439                let data_ptr = storage.as_ptr();
440                let size_in_bytes = storage.storage_size_in_bytes();
441                let data = unsafe { std::slice::from_raw_parts(data_ptr, size_in_bytes) };
442                Ok(Cow::from(data))
443            }
444            #[cfg(feature = "vulkan")]
445            QStorage::Vulkan(storage, _) => {
446                let data_ptr = storage.as_ptr();
447                let size_in_bytes = storage.storage_size_in_bytes();
448                let data = unsafe { std::slice::from_raw_parts(data_ptr, size_in_bytes) };
449                Ok(Cow::from(data))
450            }
451            #[cfg(feature = "wgpu")]
452            QStorage::Wgpu(storage, _) => {
453                let data_ptr = storage.as_ptr();
454                let size_in_bytes = storage.storage_size_in_bytes();
455                let data = unsafe { std::slice::from_raw_parts(data_ptr, size_in_bytes) };
456                Ok(Cow::from(data))
457            }
458            QStorage::Stream(_) => {
459                crate::bail!(
460                    "streaming expert bank is not resident; consume it via indexed_moe_forward"
461                )
462            }
463        }
464    }
465
466    pub fn device_ptr(&self) -> Result<*const u8> {
467        match self {
468            QStorage::Cuda(storage) => storage.device_ptr(),
469            #[cfg(feature = "rocm")]
470            QStorage::Rocm(..) => crate::bail!("not implemented"),
471            #[cfg(feature = "vulkan")]
472            QStorage::Vulkan(..) => crate::bail!("not implemented"),
473            #[cfg(feature = "wgpu")]
474            QStorage::Wgpu(..) => crate::bail!("not implemented"),
475            QStorage::Metal(_) | QStorage::Cpu(_) | QStorage::Stream(_) => {
476                crate::bail!("not implemented");
477            }
478        }
479    }
480
481    #[cfg(feature = "cuda")]
482    pub fn device_ptr_with_guard<'a>(
483        &'a self,
484        stream: &'a crate::cuda_backend::cudarc::driver::CudaStream,
485    ) -> Result<(
486        *const u8,
487        crate::cuda_backend::cudarc::driver::SyncOnDrop<'a>,
488    )> {
489        match self {
490            QStorage::Cuda(storage) => storage.device_ptr_with_guard(stream),
491            QStorage::Metal(_) | QStorage::Cpu(_) | QStorage::Stream(_) => {
492                crate::bail!("not implemented");
493            }
494        }
495    }
496}
497
498#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
499pub enum GgmlDType {
500    F32,
501    F16,
502    BF16,
503    /// Raw int32 (GGML type 26) — integer side tables like DeepSeek-V4's
504    /// `ffn_gate_tid2eid` hash-routing table. Dequantizes by casting to f32.
505    I32,
506    Q4_0,
507    Q4_1,
508    Q5_0,
509    Q5_1,
510    Q8_0,
511    Q8_1,
512    Q2K,
513    Q3K,
514    Q4K,
515    Q5K,
516    Q6K,
517    Q8K,
518    #[allow(non_camel_case_types)]
519    IQ4_NL,
520    #[allow(non_camel_case_types)]
521    IQ4_XS,
522    // 4-bit microscaling float (MXFP4), ggml type 39: 32 elems / block, E8M0 scale + 16 nibble-pairs.
523    MXFP4,
524    // IQ / ternary / 1-bit / NVFP4 codec types (decode-only; dequant->matmul on every backend).
525    #[allow(non_camel_case_types)]
526    IQ2_XXS,
527    #[allow(non_camel_case_types)]
528    IQ2_XS,
529    #[allow(non_camel_case_types)]
530    IQ3_XXS,
531    #[allow(non_camel_case_types)]
532    IQ1_S,
533    #[allow(non_camel_case_types)]
534    IQ3_S,
535    #[allow(non_camel_case_types)]
536    IQ2_S,
537    #[allow(non_camel_case_types)]
538    IQ1_M,
539    TQ1_0,
540    TQ2_0,
541    NVFP4,
542    Q1_0,
543    // ROCmFPX fork types: 4-bit, 32 elems/block, UE4M3 half-scales (100 dual, 101 fast).
544    #[allow(non_camel_case_types)]
545    ROCMFP4,
546    #[allow(non_camel_case_types)]
547    ROCMFP4_FAST,
548}
549
550// --- Single-source-of-truth wiring (Cut 2) -----------------------------------------
551//
552// The five purely 1:1-per-block `GgmlDType` methods (`from_u32`, `to_u32`, `cpu_zeros`,
553// `from_data`, `type_size`) are generated from the ONE `for_each_quant!` table in
554// `quant_format.rs` instead of six hand-maintained match blocks. Each generator below is
555// handed the whole `Variant => Block @ ggml_id` list and emits the block arms; the three
556// non-block pseudo-types (F32/F16/BF16) that the table intentionally omits keep their
557// bespoke arms inline. Behavior is byte-identical to the previous hand-written matches —
558// the table's ids/blocks were verified equal to them. (`block_size` is left hand-written:
559// the table carries no block-size data and its grouped form — F32=1, Q1_0/NVFP4 special,
560// the big QK_K group — is already minimal.)
561use crate::for_each_quant;
562
563macro_rules! gen_from_u32 {
564    ($($v:ident => $b:ident @ $id:literal),+ $(,)?) => {
565        /// The dtype for a GGML type id, the inverse of [`GgmlDType::to_u32`]. Single source of
566        /// truth (generated from the `for_each_quant!` table), so a caller that delegates here
567        /// cannot drift the way a hand-written id map does.
568        pub fn from_u32(u: u32) -> Result<Self> {
569            let dtype = match u {
570                0 => Self::F32,
571                1 => Self::F16,
572                30 => Self::BF16,
573                26 => Self::I32,
574                $( $id => Self::$v, )+
575                _ => crate::bail!("unknown dtype for tensor {u}"),
576            };
577            Ok(dtype)
578        }
579    };
580}
581
582macro_rules! gen_to_u32 {
583    ($($v:ident => $b:ident @ $id:literal),+ $(,)?) => {
584        /// GGML type id for this dtype. Single source of truth (generated from the
585        /// `for_each_quant!` table); cross-crate callers (e.g. hanzo-quant UQFF
586        /// serialization) use this instead of hand-rolled id maps that drift.
587        pub fn to_u32(self) -> u32 {
588            match self {
589                Self::F32 => 0,
590                Self::F16 => 1,
591                Self::BF16 => 30,
592                Self::I32 => 26,
593                $( Self::$v => $id, )+
594            }
595        }
596    };
597}
598
599macro_rules! gen_cpu_zeros {
600    ($($v:ident => $b:ident @ $id:literal),+ $(,)?) => {
601        /// The block dtype
602        pub fn cpu_zeros(&self, elem_count: usize) -> Box<dyn QuantizedType> {
603            match self {
604                Self::F32 => Box::new(vec![f32::zeros(); elem_count]),
605                Self::F16 => Box::new(vec![f16::zeros(); elem_count]),
606                Self::BF16 => Box::new(vec![bf16::zeros(); elem_count]),
607                Self::I32 => Box::new(vec![0i32; elem_count]),
608                $( Self::$v => Box::new(vec![<$b>::zeros(); elem_count / <$b>::BLCK_SIZE]), )+
609            }
610        }
611    };
612}
613
614macro_rules! gen_from_data {
615    ($($v:ident => $b:ident @ $id:literal),+ $(,)?) => {
616        pub fn from_data(&self, data: Cow<'_, [u8]>) -> Box<dyn QuantizedType> {
617            match self {
618                Self::F32 => Box::new(as_t_slice::<f32>(&data).to_vec()),
619                Self::F16 => Box::new(as_t_slice::<f16>(&data).to_vec()),
620                Self::BF16 => Box::new(as_t_slice::<bf16>(&data).to_vec()),
621                Self::I32 => Box::new(as_t_slice::<i32>(&data).to_vec()),
622                $( Self::$v => Box::new(as_t_slice::<$b>(&data).to_vec()), )+
623            }
624        }
625    };
626}
627
628macro_rules! gen_type_size {
629    ($($v:ident => $b:ident @ $id:literal),+ $(,)?) => {
630        /// The type size for blocks in bytes.
631        pub fn type_size(&self) -> usize {
632            use k_quants::*;
633            match self {
634                Self::F32 => 4,
635                Self::F16 | Self::BF16 => 2,
636                Self::I32 => 4,
637                $( Self::$v => std::mem::size_of::<$b>(), )+
638            }
639        }
640    };
641}
642
643macro_rules! gen_block_align {
644    ($($v:ident => $b:ident @ $id:literal),+ $(,)?) => {
645        /// Alignment (bytes) of this dtype's block type: the `T` that `from_data` and `from_mmap`
646        /// reinterpret the raw bytes as. A GGUF loader maps a tensor zero-copy only when its data
647        /// offset is a multiple of this, and otherwise copies into an owned, naturally aligned
648        /// buffer. Generated from the `for_each_quant!` table, so it covers every block dtype.
649        pub const fn block_align(&self) -> usize {
650            use k_quants::*;
651            match self {
652                Self::F32 => std::mem::align_of::<f32>(),
653                Self::F16 => std::mem::align_of::<f16>(),
654                Self::BF16 => std::mem::align_of::<bf16>(),
655                Self::I32 => std::mem::align_of::<i32>(),
656                $( Self::$v => std::mem::align_of::<$b>(), )+
657            }
658        }
659    };
660}
661
662macro_rules! gen_from_mmap {
663    ($($v:ident => $b:ident @ $id:literal),+ $(,)?) => {
664        /// No-copy CPU constructor: wrap a tensor's blocks *in place* inside the mmap'd GGUF region
665        /// (`QMmap`) instead of copying them into an owned `Vec` (the `to_vec` in `from_data`). The
666        /// returned store references the mapped pages directly, so the weight bytes stay on disk and
667        /// the OS pages them in/out under memory pressure. Mirrors `from_data` arm-for-arm so the
668        /// dtype -> block-type mapping can never disagree between the resident and mmap paths.
669        #[allow(clippy::wrong_self_convention)] // dispatches on the block-type value, mirroring from_data
670        pub(crate) fn from_mmap(
671            &self,
672            mmap: Arc<memmap2::Mmap>,
673            offset: usize,
674            n_blocks: usize,
675        ) -> Box<dyn QuantizedType> {
676            match self {
677                Self::F32 => Box::new(QMmap::<f32>::new(mmap, offset, n_blocks)),
678                Self::F16 => Box::new(QMmap::<f16>::new(mmap, offset, n_blocks)),
679                Self::BF16 => Box::new(QMmap::<bf16>::new(mmap, offset, n_blocks)),
680                Self::I32 => Box::new(QMmap::<i32>::new(mmap, offset, n_blocks)),
681                $( Self::$v => Box::new(QMmap::<$b>::new(mmap, offset, n_blocks)), )+
682            }
683        }
684    };
685}
686
687impl GgmlDType {
688    for_each_quant!(gen_from_u32);
689    for_each_quant!(gen_to_u32);
690    for_each_quant!(gen_cpu_zeros);
691    for_each_quant!(gen_from_data);
692    for_each_quant!(gen_from_mmap);
693    for_each_quant!(gen_type_size);
694    for_each_quant!(gen_block_align);
695
696    /// The block size, i.e. the number of elements stored in each block.
697    pub fn block_size(&self) -> usize {
698        match self {
699            Self::F32 => 1,
700            Self::F16 | Self::BF16 => 1,
701            Self::I32 => 1,
702            Self::Q4_0 => k_quants::QK4_0,
703            Self::Q4_1 => k_quants::QK4_1,
704            Self::Q5_0 => k_quants::QK5_0,
705            Self::Q5_1 => k_quants::QK5_1,
706            Self::Q8_0 => k_quants::QK8_0,
707            Self::Q8_1 => k_quants::QK8_1,
708            Self::IQ4_NL => k_quants::QK4_NL,
709            Self::MXFP4 => k_quants::QK_MXFP4,
710            Self::Q1_0 => iq_quants::QK1_0,
711            Self::NVFP4 => iq_quants::QK_NVFP4,
712            Self::ROCMFP4 | Self::ROCMFP4_FAST => iq_quants::QK_ROCMFP4,
713            Self::Q2K
714            | Self::Q3K
715            | Self::Q4K
716            | Self::Q5K
717            | Self::Q6K
718            | Self::Q8K
719            | Self::IQ4_XS
720            | Self::IQ2_XXS
721            | Self::IQ2_XS
722            | Self::IQ3_XXS
723            | Self::IQ1_S
724            | Self::IQ3_S
725            | Self::IQ2_S
726            | Self::IQ1_M
727            | Self::TQ1_0
728            | Self::TQ2_0 => k_quants::QK_K,
729        }
730    }
731}
732
733// A version of GgmlType without `vec_dot` so that it can be dyn boxed.
734pub trait QuantizedType: Send + Sync {
735    fn dtype(&self) -> GgmlDType;
736    fn matmul_t(&self, mkn: (usize, usize, usize), lhs: &[f32], dst: &mut [f32]) -> Result<()>;
737    fn matmul_t_f16(&self, mkn: (usize, usize, usize), lhs: &[f16], dst: &mut [f16]) -> Result<()>;
738    fn embedding(&self, ids: &[u32], rows: usize, hidden: usize) -> Result<CpuStorage>;
739    fn dequantize(&self, elem_count: usize) -> Result<CpuStorage>;
740    fn storage_size_in_bytes(&self) -> usize;
741    fn as_ptr(&self) -> *const u8;
742    fn block_size(&self) -> usize;
743    #[allow(clippy::wrong_self_convention)]
744    fn from_float(&mut self, xs: &[f32]);
745    #[allow(clippy::wrong_self_convention)]
746    fn from_float_imatrix(&mut self, xs: &[f32], imatrix_weights: &[f32], n_per_row: usize);
747    fn size(&self) -> usize;
748}
749
750/// Dequantize the rows named by `ids` out of a `[rows, hidden]` block table.
751fn embedding_rows<T: k_quants::GgmlType>(
752    blocks: &[T],
753    ids: &[u32],
754    rows: usize,
755    hidden: usize,
756) -> Result<CpuStorage> {
757    if !hidden.is_multiple_of(T::BLCK_SIZE) {
758        crate::bail!(
759            "quantized embedding hidden size {hidden} is not divisible by block size {}",
760            T::BLCK_SIZE
761        )
762    }
763    let row_blocks = hidden / T::BLCK_SIZE;
764    if blocks.len() != rows * row_blocks {
765        crate::bail!(
766            "quantized tensor has {} blocks, expected {}",
767            blocks.len(),
768            rows * row_blocks
769        )
770    }
771    let mut out = vec![0f32; ids.len() * hidden];
772    for (out_row, &row_id) in ids.iter().enumerate() {
773        let row = row_id as usize;
774        if row >= rows {
775            crate::bail!("embedding id {row} is out of range for {rows} rows")
776        }
777        let src = &blocks[row * row_blocks..(row + 1) * row_blocks];
778        let dst = &mut out[out_row * hidden..(out_row + 1) * hidden];
779        T::to_float(src, dst);
780    }
781    Ok(CpuStorage::F32(out))
782}
783
784impl<T: k_quants::GgmlType + Send + Sync> QuantizedType for Vec<T> {
785    fn matmul_t(&self, mkn: (usize, usize, usize), lhs: &[f32], dst: &mut [f32]) -> Result<()> {
786        k_quants::matmul(mkn, lhs, self.as_slice(), dst)
787    }
788    fn matmul_t_f16(&self, mkn: (usize, usize, usize), lhs: &[f16], dst: &mut [f16]) -> Result<()> {
789        k_quants::matmul_f16(mkn, lhs, self.as_slice(), dst)
790    }
791
792    fn embedding(&self, ids: &[u32], rows: usize, hidden: usize) -> Result<CpuStorage> {
793        embedding_rows(self.as_slice(), ids, rows, hidden)
794    }
795
796    fn size(&self) -> usize {
797        self.len() * core::mem::size_of::<T>()
798    }
799
800    fn from_float(&mut self, xs: &[f32]) {
801        T::from_float(xs, self)
802    }
803
804    fn from_float_imatrix(&mut self, xs: &[f32], imatrix_weights: &[f32], n_per_row: usize) {
805        T::from_float_imatrix(xs, self, imatrix_weights, n_per_row)
806    }
807
808    fn dtype(&self) -> GgmlDType {
809        T::DTYPE
810    }
811
812    fn block_size(&self) -> usize {
813        T::BLCK_SIZE
814    }
815
816    fn dequantize(&self, elem_count: usize) -> Result<CpuStorage> {
817        let mut ys = vec![0.0f32; elem_count];
818        T::to_float(self.as_slice(), &mut ys);
819        Ok(CpuStorage::F32(ys))
820    }
821
822    fn storage_size_in_bytes(&self) -> usize {
823        self.len() * std::mem::size_of::<T>()
824    }
825
826    fn as_ptr(&self) -> *const u8 {
827        self.as_ptr() as *const u8
828    }
829}
830
831/// A CPU quantized store whose blocks live in an mmap'd GGUF region rather than an owned `Vec`.
832///
833/// Holds an `Arc<memmap2::Mmap>` plus the byte offset and block count of one tensor inside it, and
834/// hands the dequant/matmul path a `&[T]` that points *directly* into the mapped file -- so the
835/// quantized weight bytes are never copied resident; the OS pages them in on access and reclaims
836/// them under memory pressure (page cache = "RAM as a speed spectrum", antirez ds4_ssd). The `Arc`
837/// keeps the mapping alive for as long as any tensor references it.
838///
839/// This is the no-copy twin of `QuantizedType for Vec<T>`: every method forwards the same
840/// `k_quants` routine over `self.as_slice()` instead of over a `Vec`. It is read-only -- the two
841/// `from_float*` (quantize) entry points are unreachable in the load path and panic if ever called
842/// (you cannot quantize *into* a memory-mapped, read-only weight file).
843pub struct QMmap<T> {
844    mmap: Arc<memmap2::Mmap>,
845    /// Byte offset of this tensor's first block within the mapping.
846    offset: usize,
847    /// Number of `T` blocks.
848    n_blocks: usize,
849    _t: std::marker::PhantomData<T>,
850}
851
852impl<T> QMmap<T> {
853    fn new(mmap: Arc<memmap2::Mmap>, offset: usize, n_blocks: usize) -> Self {
854        Self {
855            mmap,
856            offset,
857            n_blocks,
858            _t: std::marker::PhantomData,
859        }
860    }
861
862    /// The tensor's blocks as a slice into the live mapping. No copy: the pointer is inside the
863    /// mmap'd region, valid for as long as `self` (and thus the `Arc<Mmap>`) is alive. Alignment is
864    /// guaranteed by the caller (`TensorInfo::read_mmap` falls back to an owned copy for the rare
865    /// misaligned region); `as_t_slice`'s asserts are the in-place safety net.
866    #[inline]
867    fn as_slice(&self) -> &[T] {
868        let len = self.n_blocks * std::mem::size_of::<T>();
869        as_t_slice::<T>(&self.mmap[self.offset..self.offset + len])
870    }
871}
872
873impl<T: k_quants::GgmlType + Send + Sync> QuantizedType for QMmap<T> {
874    fn matmul_t(&self, mkn: (usize, usize, usize), lhs: &[f32], dst: &mut [f32]) -> Result<()> {
875        k_quants::matmul(mkn, lhs, self.as_slice(), dst)
876    }
877
878    fn matmul_t_f16(&self, mkn: (usize, usize, usize), lhs: &[f16], dst: &mut [f16]) -> Result<()> {
879        k_quants::matmul_f16(mkn, lhs, self.as_slice(), dst)
880    }
881
882    fn embedding(&self, ids: &[u32], rows: usize, hidden: usize) -> Result<CpuStorage> {
883        embedding_rows(self.as_slice(), ids, rows, hidden)
884    }
885
886    fn size(&self) -> usize {
887        self.n_blocks * std::mem::size_of::<T>()
888    }
889
890    fn from_float(&mut self, _xs: &[f32]) {
891        panic!("QMmap is read-only: cannot quantize into a memory-mapped weight region")
892    }
893
894    fn from_float_imatrix(&mut self, _xs: &[f32], _imatrix_weights: &[f32], _n_per_row: usize) {
895        panic!("QMmap is read-only: cannot quantize into a memory-mapped weight region")
896    }
897
898    fn dtype(&self) -> GgmlDType {
899        T::DTYPE
900    }
901
902    fn block_size(&self) -> usize {
903        T::BLCK_SIZE
904    }
905
906    fn dequantize(&self, elem_count: usize) -> Result<CpuStorage> {
907        let mut ys = vec![0.0f32; elem_count];
908        T::to_float(self.as_slice(), &mut ys);
909        Ok(CpuStorage::F32(ys))
910    }
911
912    fn storage_size_in_bytes(&self) -> usize {
913        self.n_blocks * std::mem::size_of::<T>()
914    }
915
916    fn as_ptr(&self) -> *const u8 {
917        self.as_slice().as_ptr() as *const u8
918    }
919}
920
921impl std::fmt::Debug for QTensor {
922    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
923        write!(f, "QTensor[{:?}; {:?}]", self.shape, self.dtype())
924    }
925}
926
927fn check_shape(shape: &Shape, block_size: usize) -> Result<()> {
928    let dims = shape.dims();
929    if dims.is_empty() {
930        crate::bail!("scalar tensor cannot be quantized {shape:?}")
931    }
932    if !dims[dims.len() - 1].is_multiple_of(block_size) {
933        crate::bail!(
934            "quantized tensor must have their last dim divisible by block size {shape:?} {}",
935            block_size
936        )
937    }
938    Ok(())
939}
940
941impl QTensor {
942    // The ONE way to build a QTensor from its parts: every constructor funnels here so a tensor's
943    // resident-bank slots start empty exactly once, alongside the bytes they are derived from.
944    fn make(storage: QStorage, shape: Shape) -> Self {
945        Self {
946            storage,
947            shape,
948            banks: ResidentBanks::default(),
949            repacked_qs: repack::PackedCache::new(),
950        }
951    }
952
953    pub fn new<S: Into<Shape>>(storage: QStorage, shape: S) -> Result<Self> {
954        let shape = shape.into();
955        check_shape(&shape, storage.block_size())?;
956        Ok(Self::make(storage, shape))
957    }
958
959    pub fn quantize(src: &Tensor, dtype: GgmlDType) -> Result<Self> {
960        let shape = src.shape();
961        let block_size = dtype.block_size();
962        check_shape(shape, block_size)?;
963        let src = src.to_dtype(crate::DType::F32)?.flatten_all()?;
964        let elem_count = shape.elem_count();
965        if !elem_count.is_multiple_of(block_size) {
966            crate::bail!(
967                "tensor size ({shape:?}) is not divisible by block size {}",
968                block_size
969            )
970        }
971        let mut storage = src.device().qzeros(elem_count, dtype)?;
972        storage.quantize(&src.storage())?;
973        Ok(Self::make(storage, shape.clone()))
974    }
975
976    pub fn quantize_imatrix(
977        src: &Tensor,
978        imatrix_weights: &[f32],
979        dtype: GgmlDType,
980    ) -> Result<Self> {
981        // (n_per_row/QK_K-1)*QK_K+(QK_K/32-1)*32+32=n_per_row
982        // Size of imatrix == last dim of tensor
983        let n_per_row = src.dim(D::Minus1)?;
984        if imatrix_weights.len() != n_per_row {
985            crate::bail!(
986                "imatrix weights must have the same length {} as the last dim of src {}",
987                imatrix_weights.len(),
988                src.dim(D::Minus1)?
989            );
990        }
991
992        let shape = src.shape();
993        let block_size = dtype.block_size();
994        check_shape(shape, block_size)?;
995        let src = src.to_dtype(crate::DType::F32)?.flatten_all()?;
996        let elem_count = shape.elem_count();
997        if !elem_count.is_multiple_of(block_size) {
998            crate::bail!(
999                "tensor size ({shape:?}) is not divisible by block size {}",
1000                block_size
1001            );
1002        }
1003        let mut storage = src.device().qzeros(elem_count, dtype)?;
1004        storage.quantize_imatrix(&src.storage(), imatrix_weights, n_per_row)?;
1005        Ok(Self::make(storage, shape.clone()))
1006    }
1007
1008    /// Quantize `src` (currently on the CPU) to a QTensor on `dev`
1009    pub fn quantize_imatrix_onto(
1010        src: &Tensor,
1011        imatrix_weights: &[f32],
1012        dtype: GgmlDType,
1013        dev: &Device,
1014    ) -> Result<Self> {
1015        if !src.device().is_cpu() {
1016            crate::bail!(
1017                "`quantize_onto` expects a `src` to be on the cpu, got {:?}.",
1018                src.device()
1019            )
1020        }
1021        // (n_per_row/QK_K-1)*QK_K+(QK_K/32-1)*32+32=n_per_row
1022        // Size of imatrix == last dim of tensor
1023        let n_per_row = src.dim(D::Minus1)?;
1024        if imatrix_weights.len() != n_per_row {
1025            crate::bail!(
1026                "imatrix weights must have the same length {} as the last dim of src {}",
1027                imatrix_weights.len(),
1028                src.dim(D::Minus1)?
1029            );
1030        }
1031        let shape = src.shape();
1032        let block_size = dtype.block_size();
1033        check_shape(shape, block_size)?;
1034        let src = src.to_dtype(crate::DType::F32)?.flatten_all()?;
1035        let elem_count = shape.elem_count();
1036        if !elem_count.is_multiple_of(block_size) {
1037            crate::bail!(
1038                "tensor size ({shape:?}) is not divisible by block size {}",
1039                block_size
1040            )
1041        }
1042        // storage is on the `dev`, src is on `cpu`
1043        let mut storage = dev.qzeros(elem_count, dtype)?;
1044        storage.quantize_imatrix_onto(&src.storage(), imatrix_weights, n_per_row)?;
1045        Ok(Self::make(storage, shape.clone()))
1046    }
1047
1048    /// Quantize `src` (currently on the CPU) to a QTensor on `dev`
1049    pub fn quantize_onto(src: &Tensor, dtype: GgmlDType, dev: &Device) -> Result<Self> {
1050        if !src.device().is_cpu() {
1051            crate::bail!(
1052                "`quantize_onto` expects a `src` to be on the cpu, got {:?}.",
1053                src.device()
1054            )
1055        }
1056        let shape = src.shape();
1057        let block_size = dtype.block_size();
1058        check_shape(shape, block_size)?;
1059        let src = src.to_dtype(crate::DType::F32)?.flatten_all()?;
1060        let elem_count = shape.elem_count();
1061        if !elem_count.is_multiple_of(block_size) {
1062            crate::bail!(
1063                "tensor size ({shape:?}) is not divisible by block size {}",
1064                block_size
1065            )
1066        }
1067        // storage is on the `dev`, src is on `cpu`
1068        let mut storage = dev.qzeros(elem_count, dtype)?;
1069        storage.quantize_onto(&src.storage())?;
1070        Ok(Self::make(storage, shape.clone()))
1071    }
1072
1073    pub fn dtype(&self) -> GgmlDType {
1074        self.storage.dtype()
1075    }
1076
1077    pub fn device(&self) -> Device {
1078        self.storage.device()
1079    }
1080
1081    pub fn rank(&self) -> usize {
1082        self.shape.rank()
1083    }
1084
1085    pub fn shape(&self) -> &Shape {
1086        &self.shape
1087    }
1088
1089    pub fn dequantize(&self, device: &Device) -> Result<Tensor> {
1090        let storage = self.storage.dequantize(self.shape.elem_count())?;
1091        let none = crate::op::BackpropOp::none();
1092        crate::tensor::from_storage(storage, self.shape.clone(), none, false).to_device(device)
1093    }
1094
1095    pub fn dequantize_f16(&self, device: &Device) -> Result<Tensor> {
1096        // In the CUDA case, we have a specialized kernel as this can be useful for volta
1097        // architectures. https://github.com/hanzoai/ml/issues/2136
1098        match &self.storage {
1099            QStorage::Cuda(s) => {
1100                let s = s.dequantize_f16(self.shape.elem_count())?;
1101                let none = crate::op::BackpropOp::none();
1102                crate::tensor::from_storage(Storage::Cuda(s), self.shape.clone(), none, false)
1103                    .to_device(device)
1104            }
1105            _ => {
1106                let s = self.dequantize(device)?.to_dtype(crate::DType::F16)?;
1107                Ok(s)
1108            }
1109        }
1110    }
1111
1112    pub fn embedding(&self, ids: &Tensor) -> Result<Tensor> {
1113        let (rows, hidden) = self.shape.dims2()?;
1114        if !hidden.is_multiple_of(self.dtype().block_size()) {
1115            crate::bail!(
1116                "quantized embedding hidden size {hidden} is not divisible by block size {}",
1117                self.dtype().block_size()
1118            )
1119        }
1120        let mut out_shape = ids.dims().to_vec();
1121        out_shape.push(hidden);
1122        let device = self.device();
1123        let ids = ids
1124            .to_device(&device)?
1125            .to_dtype(DType::U32)?
1126            .flatten_all()?
1127            .contiguous()?;
1128        let storage = match &self.storage {
1129            QStorage::Cpu(storage) => {
1130                let ids = ids.to_vec1::<u32>()?;
1131                Storage::Cpu(storage.embedding(&ids, rows, hidden)?)
1132            }
1133            QStorage::Metal(storage) => match &*ids.storage() {
1134                Storage::Metal(ids_storage) => {
1135                    Storage::Metal(storage.embedding(rows, hidden, ids_storage, ids.layout())?)
1136                }
1137                _ => unreachable!("ids were moved to the QTensor device"),
1138            },
1139            QStorage::Cuda(storage) => match &*ids.storage() {
1140                Storage::Cuda(ids_storage) => {
1141                    Storage::Cuda(storage.embedding(rows, hidden, ids_storage, ids.layout())?)
1142                }
1143                _ => unreachable!("ids were moved to the QTensor device"),
1144            },
1145            #[cfg(feature = "rocm")]
1146            QStorage::Rocm(..) => return self.embedding_dequantized(&ids, out_shape),
1147            #[cfg(feature = "vulkan")]
1148            QStorage::Vulkan(..) => return self.embedding_dequantized(&ids, out_shape),
1149            #[cfg(feature = "wgpu")]
1150            QStorage::Wgpu(..) => return self.embedding_dequantized(&ids, out_shape),
1151            QStorage::Stream(_) => {
1152                crate::bail!("streaming expert bank is not resident; it has no embedding rows")
1153            }
1154        };
1155        let none = crate::op::BackpropOp::none();
1156        Ok(crate::tensor::from_storage(storage, out_shape, none, false))
1157    }
1158
1159    /// Row gather through a dequantized table, for backends without a quantized get_rows kernel.
1160    #[cfg(any(feature = "rocm", feature = "vulkan", feature = "wgpu"))]
1161    fn embedding_dequantized(&self, ids: &Tensor, out_shape: Vec<usize>) -> Result<Tensor> {
1162        self.dequantize(ids.device())?
1163            .index_select(ids, 0)?
1164            .reshape(out_shape)
1165    }
1166
1167    pub fn storage_size_in_bytes(&self) -> usize {
1168        self.storage.size_in_bytes()
1169    }
1170
1171    pub fn data(&self) -> Result<Cow<'_, [u8]>> {
1172        self.storage.data()
1173    }
1174
1175    // Upload this expert bank's GGML bytes to VRAM ONCE and keep it resident, keyed by the stable
1176    // (ptr,len) of the QTensor's CPU bytes. Re-uploading the multi-GB bank per token/layer would
1177    // dominate decode, so the cache makes MoE bandwidth-bound on the quant matvec, not the H2D copy.
1178    #[cfg(feature = "rocm")]
1179    fn rocm_moe_bank(&self, dev: &crate::RocmDevice) -> Result<std::sync::Arc<crate::RocmStorage>> {
1180        use crate::backend::BackendDevice;
1181        let bank = self.data()?;
1182        cache_or_upload(&self.banks.rocm, bank.as_ref(), |b| {
1183            dev.storage_from_slice(b)
1184        })
1185    }
1186
1187    // Resident Vulkan MoE expert bank (twin of `rocm_moe_bank`). Q8_0 repacks to the 9-u32/block
1188    // layout and Q6_K to the padded 53-u32 super-block layout their MoE shaders read (mirrors the 2D
1189    // decode paths); Q4_0/Q4_K shaders byte-address the raw GGML bytes, so a plain upload suffices.
1190    #[cfg(feature = "vulkan")]
1191    fn vulkan_moe_bank(
1192        &self,
1193        dev: &crate::VulkanDevice,
1194        e_cnt: usize,
1195        n: usize,
1196        k: usize,
1197    ) -> Result<std::sync::Arc<crate::VulkanStorage>> {
1198        let bank = self.data()?;
1199        let dt = self.storage.dtype();
1200        cache_or_upload(&self.banks.vulkan, bank.as_ref(), |b| match dt {
1201            GgmlDType::Q8_0 => dev.quantize_q8_blocks(b, e_cnt * n, k),
1202            GgmlDType::Q6K => dev.quantize_q6k(b, e_cnt * n, k),
1203            _ => dev.upload_qweight(b),
1204        })
1205    }
1206
1207    // Resident PLANAR expert bank for the DSL block-reduced MoE kernels: the packed GGML bank is
1208    // de-interleaved into per-field device arrays once (at first sight of its CPU bytes) and reused
1209    // every token. Only Q4_K/Q6_K -- the dtypes with a committed `moe_matvec_q*k_blk_*` .spv.
1210    #[cfg(feature = "vulkan")]
1211    fn vulkan_moe_bank_split(
1212        &self,
1213        dev: &crate::VulkanDevice,
1214        e_cnt: usize,
1215        n: usize,
1216        k: usize,
1217    ) -> Result<std::sync::Arc<crate::vulkan::MoeBankSplit>> {
1218        let bank = self.data()?;
1219        let dt = self.storage.dtype();
1220        cache_or_upload(&self.banks.vulkan_split, bank.as_ref(), |b| match dt {
1221            GgmlDType::Q4K => dev.quantize_q4k_split(b, e_cnt * n, k),
1222            GgmlDType::Q6K => dev.quantize_q6k_split(b, e_cnt * n, k),
1223            _ => crate::bail!("vulkan_moe_bank_split: unsupported dtype {dt:?}"),
1224        })
1225    }
1226
1227    // Resident wgpu MoE expert bank (twin of `vulkan_moe_bank`). The wgpu MoE shaders byte-address
1228    // the raw GGML bytes for every native type, so one upload path covers Q4_0/Q8_0/Q4K.
1229    #[cfg(feature = "wgpu")]
1230    fn wgpu_moe_bank(&self, dev: &crate::WgpuDevice) -> Result<std::sync::Arc<crate::WgpuStorage>> {
1231        let bank = self.data()?;
1232        cache_or_upload(&self.banks.wgpu, bank.as_ref(), |b| dev.upload_qweight(b))
1233    }
1234
1235    pub fn indexed_moe_forward(&self, x: &Tensor, ids: &Tensor) -> Result<Tensor> {
1236        // The fused CUDA path reads ids as a flat row-major [batch*topk] buffer (rank-agnostic since
1237        // 0.11.17); force dense strides so a non-contiguous router output can't misindex the kernel.
1238        let ids = &ids.contiguous()?;
1239        match &self.storage {
1240            // Only dtypes with a fused CUDA indexed-MoE kernel take the fast path; others (e.g. MXFP4,
1241            // i-quant/ternary) fall through to the generic per-expert path below, which dequantizes via
1242            // QMatMul. The supported set is derived from the ONE kernel-name table (cuda.rs), so this gate
1243            // can't drift from the kernels that actually exist -- which is exactly what had stranded
1244            // Q4_0/Q4_1/Q5_0/Q5_1 on the CPU even though their fused kernels are now compiled.
1245            QStorage::Cuda(s) if cuda::QCudaStorage::supports_indexed_moe(s.dtype()) => {
1246                // The fused q8_1 MoE kernel reads the activation as f32 (as_cuda_slice::<f32>), so a
1247                // BF16/F16 compute dtype must ride f32 into the kernel and restore on the way out --
1248                // the same reconciliation the i-quant and Vulkan sibling branches already perform. A
1249                // f32 model no-ops both casts.
1250                let out_dtype = x.dtype();
1251                let x = x.to_dtype(crate::DType::F32)?.contiguous()?;
1252                // Bind the storage guards to named locals (declared after `x`, dropped before it)
1253                // so the borrow can't outlive the reconciled activation -- mirrors the Metal branch.
1254                let (x_guard, x_l) = x.storage_and_layout();
1255                let (ids_guard, ids_l) = ids.storage_and_layout();
1256                match (&*x_guard, &*ids_guard) {
1257                    (Storage::Cuda(x_storage), Storage::Cuda(ids_storage)) => {
1258                        let (storage, out_shape) = s.indexed_moe_forward(
1259                            self.shape(),
1260                            x_storage,
1261                            x_l,
1262                            ids_storage,
1263                            ids_l,
1264                        )?;
1265                        crate::tensor::from_storage(
1266                            Storage::Cuda(storage),
1267                            out_shape,
1268                            crate::op::BackpropOp::none(),
1269                            false,
1270                        )
1271                        .to_dtype(out_dtype)
1272                    }
1273                    _ => {
1274                        panic!("Non-cuda indexed_moe_forward is not implemented!");
1275                    }
1276                }
1277            }
1278            // Native CUDA i-quant MoE: the i-quant codebook types have no Blue-C fused q8_1 MoE kernel,
1279            // but a native dp4a MoE-decode kernel (moe_qmatvec_dp4a_<iq*>). The [E,n,k] bank stays
1280            // RESIDENT in VRAM and the router gather runs on-device -- the dp4a twin of the ROCm path.
1281            // This intercepts i-quant MoE BEFORE the generic fallback below, which would DtoH the whole
1282            // expert bank to host (self.data()) and re-upload every selected expert PER TOKEN.
1283            #[cfg(feature = "cuda")]
1284            QStorage::Cuda(s) if cuda::QCudaStorage::supports_iquant_moe(s.dtype()) => {
1285                let out_dtype = x.dtype();
1286                let (_e_cnt, n, k) = self.shape().dims3()?;
1287                let (t, topk) = ids.dims2()?;
1288                let nrows = t * topk;
1289
1290                // PREFILL (t>1): expert-grouped int8-WMMA MMQ (qmmq) -- stage each expert's weight ONCE
1291                // and amortize it over all its routed tokens via the tensor cores (llama mul_mat_id),
1292                // instead of the per-slot dp4a re-streaming the weight per token. Uses the RAW [t,in1,k]
1293                // input (indexed_moe_grouped broadcasts/gathers internally). IQ1_M (no MMQ kernel) returns
1294                // None -> the per-slot dp4a below.
1295                if t > 1 {
1296                    let x_f32 = x.to_dtype(crate::DType::F32)?.contiguous()?;
1297                    let ids_u32 = ids.to_dtype(crate::DType::U32)?.contiguous()?;
1298                    let (xs, _) = x_f32.storage_and_layout();
1299                    let xc = match &*xs {
1300                        Storage::Cuda(c) => c,
1301                        _ => crate::bail!("cuda i-quant MoE: x not on cuda after contiguous()"),
1302                    };
1303                    let (ids_s, _) = ids_u32.storage_and_layout();
1304                    let idc = match &*ids_s {
1305                        Storage::Cuda(c) => c,
1306                        _ => crate::bail!("cuda i-quant MoE: ids not on cuda"),
1307                    };
1308                    if let Some((st, sh)) = s.moe_iquant_qmmq(
1309                        self.shape(),
1310                        xc.as_cuda_slice::<f32>()?,
1311                        x.shape(),
1312                        &idc.as_cuda_slice::<u32>()?.slice(0..),
1313                        ids.shape(),
1314                    )? {
1315                        return crate::tensor::from_storage(
1316                            Storage::Cuda(st),
1317                            sh,
1318                            crate::op::BackpropOp::none(),
1319                            false,
1320                        )
1321                        .to_dtype(out_dtype);
1322                    }
1323                }
1324
1325                // DECODE (t==1) or qmmq-unsupported (IQ1_M): per-slot dp4a. Broadcast the shared gate/up
1326                // input across topk to a per-slot [nrows,k] activation. f32-native -> the matvec returns
1327                // F32 and the to_dtype below is a no-op for f32 models.
1328                let sdim = x.dim(1)?; // 1 (gate/up: shared input) or topk (down: per-slot)
1329                let x_exp = if sdim == topk {
1330                    x.clone()
1331                } else {
1332                    x.broadcast_as((t, topk, k))?
1333                };
1334                let x_flat = x_exp
1335                    .reshape((nrows, k))?
1336                    .to_dtype(crate::DType::F32)?
1337                    .contiguous()?;
1338                let ids_flat = ids
1339                    .reshape((nrows,))?
1340                    .to_dtype(crate::DType::U32)?
1341                    .contiguous()?;
1342                let (xstore, _) = x_flat.storage_and_layout();
1343                let xc = match &*xstore {
1344                    Storage::Cuda(c) => c,
1345                    _ => crate::bail!("cuda i-quant MoE: x not on cuda after contiguous()"),
1346                };
1347                let (idstore, _) = ids_flat.storage_and_layout();
1348                let idc = match &*idstore {
1349                    Storage::Cuda(c) => c,
1350                    _ => crate::bail!("cuda i-quant MoE: ids not on cuda"),
1351                };
1352                let y = s.moe_iquant_dp4a(
1353                    &xc.as_cuda_slice::<f32>()?.slice(0..),
1354                    &idc.as_cuda_slice::<u32>()?.slice(0..),
1355                    nrows,
1356                    n,
1357                    k,
1358                )?;
1359                let out = crate::tensor::from_storage(
1360                    Storage::Cuda(y),
1361                    (nrows, n),
1362                    crate::op::BackpropOp::none(),
1363                    false,
1364                );
1365                out.reshape((t, topk, n))?.to_dtype(out_dtype)
1366            }
1367            // Native Vulkan MoE: one fused grouped quant matvec dispatch reads the per-expert slice
1368            // out of the GGML weight bank [E, n, k] resident in VRAM and gathers by the router ids --
1369            // the whole expert compute runs on the GPU (no CPU expert loop; the CPU fallback below
1370            // would also hit the unimplemented Vulkan index_add). Supported for Q4_0/Q8_0/Q4_K; other
1371            // quant dtypes fall through to the (CPU-bound) generic path.
1372            #[cfg(feature = "vulkan")]
1373            QStorage::Vulkan(_, vk_dev) if vk_moe_kernel(self.storage.dtype()).is_some() => {
1374                let out_dtype = x.dtype();
1375                let (e_cnt, n, k) = self.shape().dims3()?;
1376                let (t, topk) = ids.dims2()?;
1377                let s = x.dim(1)?; // 1 (gate/up: shared input) or topk (down: per-slot)
1378                let x_exp = if s == topk {
1379                    x.clone()
1380                } else {
1381                    x.broadcast_as((t, topk, k))?
1382                };
1383                // [S, k] contiguous f32 on the Vulkan device; S = t*topk routed slots.
1384                let nrows = t * topk;
1385                let x_flat = x_exp
1386                    .reshape((nrows, k))?
1387                    .to_dtype(crate::DType::F32)?
1388                    .contiguous()?;
1389                // Keep routing ids on the GPU. They came from `moe_route`'s on-device sort, so reading
1390                // them to host (`to_vec1`) and re-uploading forced a GPU->CPU->GPU sync on EVERY expert
1391                // call -- 3x/layer (gate, up, down), ~144 fence stalls/token that serialized the whole
1392                // forward (the GPU sat idle-waiting). Bind the resident U32 buffer directly; the router
1393                // selects topk over exactly `e_cnt` logits, so ids are in-range by construction (the old
1394                // host-side OOB scan was the round-trip's only excuse).
1395                let dt = self.storage.dtype();
1396                let ids_u32 = ids
1397                    .reshape((nrows,))?
1398                    .to_dtype(crate::DType::U32)?
1399                    .contiguous()?;
1400                let y = {
1401                    let (store, _) = x_flat.storage_and_layout();
1402                    let xv = match &*store {
1403                        Storage::Vulkan(v) => v,
1404                        _ => crate::bail!("vulkan MoE: x not on vulkan after contiguous()"),
1405                    };
1406                    let (ids_store, _) = ids_u32.storage_and_layout();
1407                    let ids_v = match &*ids_store {
1408                        Storage::Vulkan(v) => v,
1409                        _ => crate::bail!("vulkan MoE: ids not on vulkan after contiguous()"),
1410                    };
1411                    // PREFILL (t > 1) routes to the expert-grouped MMQ on the matrix cores: each
1412                    // expert's weight is streamed ONCE and amortised over all of its routed tokens,
1413                    // where the per-slot matvec re-streams it per token and leaves the matrix cores
1414                    // idle. Twin of the CUDA/ROCm/Metal `t > 1` arms above. DECODE (t == 1) stays on
1415                    // the matvec -- one slot per expert means there is no reuse to win, and that path
1416                    // already runs at the memory wall. Q4_K only: the Q6_K `_dn` half of a Q4_K_M MoE
1417                    // has no MMQ kernel yet and keeps the matvec, which is correct at any token count.
1418                    // VK_MOE_PREFILL_GEMM_OFF forces the matvec for the A/B.
1419                    if t > 1
1420                        && dt == GgmlDType::Q4K
1421                        && vk_dev.has_int_dot8()
1422                        && std::env::var_os("VK_MOE_PREFILL_GEMM_OFF").is_none()
1423                    {
1424                        let bank = self.vulkan_moe_bank_split(vk_dev, e_cnt, n, k)?;
1425                        // Gate and up share this routed activation; quantizing here keeps the q8
1426                        // conversion identical to the decode matvec's, so the two paths differ only
1427                        // in how the weight is streamed.
1428                        let (xq, xsq, xsum) = vk_dev.quantize_act_q8(xv, nrows, k)?;
1429                        // cap = t: a token's top-k experts are distinct, so an expert claims at most
1430                        // one of that token's slots.
1431                        vk_dev.mmq_q4k_id_gpu(
1432                            bank.as_ref(),
1433                            &xq,
1434                            &xsq,
1435                            &xsum,
1436                            ids_v,
1437                            nrows,
1438                            e_cnt,
1439                            t,
1440                            n,
1441                            k,
1442                        )?
1443                    } else {
1444                        // Prefer the dp4a (int8 OpSDot) block kernel when the device supports integer
1445                        // dot-product and a specialized .spv exists (~1.4-1.6x the f32-decode block kernel,
1446                        // within activation-quant tolerance). Else the f32 DSL block kernel (planar bank,
1447                        // one workgroup/output, ~2-3x packed). Else the packed `vk_moe_kernel`. Same split
1448                        // bank feeds both block paths; the dp4a path q8-quantizes the activation once.
1449                        match vk_moe_blk_dp4a_kernel(dt, n, k).filter(|_| vk_dev.has_int_dot8()) {
1450                            Some((blk, with_xsum)) => {
1451                                let bank = self.vulkan_moe_bank_split(vk_dev, e_cnt, n, k)?;
1452                                vk_dev.moe_matvec_blk_dp4a_gpu(
1453                                    blk,
1454                                    with_xsum,
1455                                    bank.as_ref(),
1456                                    xv,
1457                                    ids_v,
1458                                    nrows,
1459                                    n,
1460                                    k,
1461                                )?
1462                            }
1463                            None => match vk_moe_blk_kernel(dt, n, k) {
1464                                Some(blk) => {
1465                                    let bank = self.vulkan_moe_bank_split(vk_dev, e_cnt, n, k)?;
1466                                    vk_dev.moe_matvec_blk_gpu(
1467                                        blk,
1468                                        bank.as_ref(),
1469                                        xv,
1470                                        ids_v,
1471                                        nrows,
1472                                        n,
1473                                        k,
1474                                    )?
1475                                }
1476                                None => {
1477                                    // Guarded by `vk_moe_kernel(..).is_some()`, so the packed kernel is present.
1478                                    let kernel = vk_moe_kernel(dt).unwrap();
1479                                    let wbank = self.vulkan_moe_bank(vk_dev, e_cnt, n, k)?;
1480                                    vk_dev.moe_matvec_gpu(
1481                                        kernel,
1482                                        wbank.as_ref(),
1483                                        xv,
1484                                        ids_v,
1485                                        nrows,
1486                                        n,
1487                                        k,
1488                                    )?
1489                                }
1490                            },
1491                        }
1492                    }
1493                };
1494                let out = crate::tensor::from_storage(
1495                    Storage::Vulkan(y),
1496                    (nrows, n),
1497                    crate::op::BackpropOp::none(),
1498                    false,
1499                );
1500                out.reshape((t, topk, n))?.to_dtype(out_dtype)
1501            }
1502            // Native wgpu MoE: mirror of the Vulkan fused grouped quant matvec dispatch. The GGML
1503            // weight bank [E, n, k] is uploaded once and the router gather + per-expert GEMM run in
1504            // one WGSL dispatch. Supported for Q4_0/Q8_0/Q4_K.
1505            #[cfg(feature = "wgpu")]
1506            QStorage::Wgpu(_, wgpu_dev) if wgpu_moe_kernel(self.storage.dtype()).is_some() => {
1507                let out_dtype = x.dtype();
1508                let (e_cnt, n, k) = self.shape().dims3()?;
1509                let (t, topk) = ids.dims2()?;
1510                let s = x.dim(1)?; // 1 (gate/up: shared input) or topk (down: per-slot)
1511                let x_exp = if s == topk {
1512                    x.clone()
1513                } else {
1514                    x.broadcast_as((t, topk, k))?
1515                };
1516                let nrows = t * topk;
1517                let x_flat = x_exp
1518                    .reshape((nrows, k))?
1519                    .to_dtype(crate::DType::F32)?
1520                    .contiguous()?;
1521                let ids_vec = ids
1522                    .reshape((nrows,))?
1523                    .to_dtype(crate::DType::U32)?
1524                    .to_vec1::<u32>()?;
1525                if let Some(&bad) = ids_vec.iter().find(|&&e| e as usize >= e_cnt) {
1526                    crate::bail!("indexed_moe_forward: expert id {bad} >= num_experts {e_cnt}");
1527                }
1528                // Guarded by `wgpu_moe_kernel(..).is_some()` above, so the kernel is always present.
1529                let kernel = wgpu_moe_kernel(self.storage.dtype()).unwrap();
1530                // Resident bank: uploaded once, reused every token (see `wgpu_moe_bank`).
1531                let wbank = self.wgpu_moe_bank(wgpu_dev)?;
1532                let ids_buf = wgpu_dev.upload_ids(&ids_vec)?;
1533                let y = {
1534                    let (store, _) = x_flat.storage_and_layout();
1535                    let xv = match &*store {
1536                        Storage::Wgpu(v) => v,
1537                        _ => crate::bail!("wgpu MoE: x not on wgpu after contiguous()"),
1538                    };
1539                    wgpu_dev.moe_matvec_gpu(kernel, wbank.as_ref(), xv, &ids_buf, nrows, n, k)?
1540                };
1541                let out = crate::tensor::from_storage(
1542                    Storage::Wgpu(y),
1543                    (nrows, n),
1544                    crate::op::BackpropOp::none(),
1545                    false,
1546                );
1547                out.reshape((t, topk, n))?.to_dtype(out_dtype)
1548            }
1549            // Native ROCm MoE: the GGML expert bank [E,n,k] is uploaded once and each routed slot
1550            // is dispatched through the SAME unified qmatvec_core<WTYPE> as ordinary decode (no
1551            // MoE-per-quant kernel; works for every wired quant). Avoids ROCm's missing index_add:
1552            // each routed slot writes exactly one output row, placed directly by slot index.
1553            #[cfg(feature = "rocm")]
1554            QStorage::Rocm(_, rocm_dev)
1555                if crate::RocmQuantType::from_ggml(self.storage.dtype()).is_some() =>
1556            {
1557                let qt = crate::RocmQuantType::from_ggml(self.storage.dtype()).unwrap();
1558                let out_dtype = x.dtype();
1559                // e_cnt is not read: ids stay on-device and the router guarantees the bound, so the
1560                // old host bounds check (and its DtoH `to_vec1`) is gone.
1561                let (_e_cnt, n, k) = self.shape().dims3()?;
1562                let (t, topk) = ids.dims2()?;
1563                let s = x.dim(1)?; // 1 (gate/up: shared input) or topk (down: per-slot)
1564                let x_exp = if s == topk {
1565                    x.clone()
1566                } else {
1567                    x.broadcast_as((t, topk, k))?
1568                };
1569                let nrows = t * topk;
1570                // PREFILL (t>1) routes to the fused expert-grouped WMMA GEMM (f16 activations); DECODE
1571                // (t==1) keeps the model's native bf16/f16 on the capture-clean matvec. See the twin in
1572                // QStorage::indexed_moe_forward.
1573                // Decode-only types (no qmmq kernel) ride the per-slot matvec core for prefill too
1574                // (correct at any token count). ONE predicate gates every prefill site.
1575                let use_qmmq = t > 1 && qt.qmmq_capable();
1576                let x_flat = match x_exp.dtype() {
1577                    // qmmq quantizes f16/f32 activations natively, so keep the model's dtype and skip
1578                    // the f32->f16 cast (a 16.7M-elem read+write per gate/up). Other dtypes (bf16 with
1579                    // a symmetric expert type) still cast to f16.
1580                    DType::F16 | DType::F32 if use_qmmq => {
1581                        x_exp.reshape((nrows, k))?.contiguous()?
1582                    }
1583                    _ if use_qmmq => x_exp
1584                        .reshape((nrows, k))?
1585                        .to_dtype(DType::F16)?
1586                        .contiguous()?,
1587                    DType::BF16 | DType::F16 => x_exp.reshape((nrows, k))?.contiguous()?,
1588                    // DECODE f32-native: dp4a experts quantize q8_1 from f32 and store f32, so an F32
1589                    // routed activation stays F32 (the matvec returns F32 -> the .to_dtype(out_dtype)
1590                    // below is a no-op), removing the cast pair that wrapped each gate/up/down matvec.
1591                    DType::F32 if qt.dp4a_active() => x_exp.reshape((nrows, k))?.contiguous()?,
1592                    _ => x_exp
1593                        .reshape((nrows, k))?
1594                        .to_dtype(DType::F16)?
1595                        .contiguous()?,
1596                };
1597                let wbank = self.rocm_moe_bank(rocm_dev)?;
1598                // Keep router ids ON the GPU for EVERY wired quant type and run ONE batched launch
1599                // (experts on grid.y, ids read on-device). No `to_vec1` DtoH sync -- that host round-
1600                // trip (3 per layer x 48 layers per token) was both the dominant WSL decode stall AND
1601                // what made HIP stream capture illegal (hipErrorStreamCaptureImplicit -> the graph-path
1602                // SIGSEGV). The router emits a top-k over the e_cnt expert logits, so 0 <= id < e_cnt
1603                // by construction; the prior host bounds check is dropped to stay capture-clean.
1604                let ids_u32 = ids
1605                    .reshape((nrows,))?
1606                    .to_dtype(crate::DType::U32)?
1607                    .contiguous()?;
1608                let (store, _) = x_flat.storage_and_layout();
1609                let xr = match &*store {
1610                    crate::Storage::Rocm(r) => r,
1611                    _ => crate::bail!("rocm MoE: x not on rocm after contiguous()"),
1612                };
1613                let (idstore, _) = ids_u32.storage_and_layout();
1614                let idr = match &*idstore {
1615                    crate::Storage::Rocm(r) => r,
1616                    _ => crate::bail!("rocm MoE: ids not on rocm"),
1617                };
1618                let y = if use_qmmq {
1619                    rocm_dev.moe_qmmq_quant(qt, wbank.as_ref(), xr, idr, nrows, n, k)?
1620                } else {
1621                    rocm_dev.moe_matvec_quant(qt, wbank.as_ref(), xr, idr, nrows, n, k)?
1622                };
1623                let out = crate::tensor::from_storage(
1624                    crate::Storage::Rocm(y),
1625                    (nrows, n),
1626                    crate::op::BackpropOp::none(),
1627                    false,
1628                );
1629                out.reshape((t, topk, n))?.to_dtype(out_dtype)
1630            }
1631            // Native Metal MoE: `QMetalStorage::indexed_moe_forward` runs the whole expert compute in
1632            // ONE fused dispatch straight out of the resident quantized bank -- decode (t == 1) via
1633            // `mul_mv_id` (per-slot matvec), prefill (t > 1) via `mul_mm_id` (expert-grouped GEMM, each
1634            // expert's weight read once and amortized over its tokens). Expert id read per row
1635            // on-device, no per-expert host loop, no `ids` DtoH sync. Same low buffer churn on both
1636            // paths (x_f32 + ids_u32 + dst), unlike the generic per-expert fallback whose per-expert
1637            // `from_data`+forward churned the Metal pool. Guarded to Metal-resident x/ids.
1638            #[cfg(feature = "metal")]
1639            QStorage::Metal(s)
1640                if matches!(&*x.storage(), Storage::Metal(_))
1641                    && matches!(&*ids.storage(), Storage::Metal(_)) =>
1642            {
1643                let out_dtype = x.dtype();
1644                let x = x.contiguous()?;
1645                let (xs_guard, x_l) = x.storage_and_layout();
1646                let (ids_guard, ids_l) = ids.storage_and_layout();
1647                let (Storage::Metal(x_storage), Storage::Metal(ids_storage)) =
1648                    (&*xs_guard, &*ids_guard)
1649                else {
1650                    unreachable!("metal MoE arm is guarded on Metal x/ids storage");
1651                };
1652                let (storage, out_shape) =
1653                    s.indexed_moe_forward(self.shape(), x_storage, x_l, ids_storage, ids_l)?;
1654                let out = crate::tensor::from_storage(
1655                    Storage::Metal(storage),
1656                    out_shape,
1657                    crate::op::BackpropOp::none(),
1658                    false,
1659                );
1660                out.to_dtype(out_dtype)
1661            }
1662            // Disk-streaming bank: same per-expert quantized matmul as the resident fallback below,
1663            // but each selected expert's [n, k] slice is fetched through the pin/LRU cache (disk)
1664            // instead of sliced out of a resident blob. Bit-identical -- only the fetch path differs.
1665            QStorage::Stream(bank) => {
1666                let (e_cnt, n, k) = self.shape().dims3()?;
1667                let dtype = bank.dtype();
1668                moe_grouped_per_expert(x, ids, n, k, |eid, device| {
1669                    if eid as usize >= e_cnt {
1670                        crate::bail!("indexed_moe_forward: expert id {eid} >= num_experts {e_cnt}");
1671                    }
1672                    let bytes = bank.fetch(eid)?;
1673                    QStorage::from_data(std::borrow::Cow::Borrowed(&bytes), device, dtype)
1674                })
1675            }
1676            _ => {
1677                // CPU / non-CUDA fallback: per-expert quantized matmul. The packed expert bank
1678                // [E, n, k] is sliced into equal, contiguous per-expert quantized blocks; for
1679                // each expert that is actually selected we run hanzo-ml's native quantized matmul
1680                // on just the tokens routed to it. Nothing is dequantized, so quantized MoE runs
1681                // on any backend (CPU, Metal, ...) at a cost proportional to the active experts.
1682                let (e_cnt, n, k) = self.shape().dims3()?;
1683                let dtype = self.storage.dtype();
1684                let all_bytes = self.data()?;
1685                let expert_bytes = all_bytes.len() / e_cnt;
1686                moe_grouped_per_expert(x, ids, n, k, |eid, device| {
1687                    let off = eid as usize * expert_bytes;
1688                    QStorage::from_data(
1689                        std::borrow::Cow::Borrowed(&all_bytes[off..off + expert_bytes]),
1690                        device,
1691                        dtype,
1692                    )
1693                })
1694            }
1695        }
1696    }
1697
1698    pub fn device_ptr(&self) -> Result<*const u8> {
1699        match &self.storage {
1700            QStorage::Cuda(storage) => storage.device_ptr(),
1701            #[cfg(feature = "rocm")]
1702            QStorage::Rocm(..) => crate::bail!("not implemented"),
1703            #[cfg(feature = "vulkan")]
1704            QStorage::Vulkan(..) => crate::bail!("not implemented"),
1705            #[cfg(feature = "wgpu")]
1706            QStorage::Wgpu(..) => crate::bail!("not implemented"),
1707            QStorage::Metal(_) | QStorage::Cpu(_) | QStorage::Stream(_) => {
1708                crate::bail!("not implemented");
1709            }
1710        }
1711    }
1712
1713    #[cfg(feature = "cuda")]
1714    pub fn device_ptr_with_guard<'a>(
1715        &'a self,
1716        stream: &'a crate::cuda_backend::cudarc::driver::CudaStream,
1717    ) -> Result<(
1718        *const u8,
1719        crate::cuda_backend::cudarc::driver::SyncOnDrop<'a>,
1720    )> {
1721        self.storage.device_ptr_with_guard(stream)
1722    }
1723}
1724
1725#[derive(Clone, Debug)]
1726pub enum QMatMul {
1727    QTensor(std::sync::Arc<QTensor>),
1728    Tensor(Tensor),
1729    TensorF16(Tensor),
1730    // Native Vulkan quantized weight: the GGML quantized blocks live in VRAM (Q4_0/Q4_K ~0.5 B/elem,
1731    // Q8_0 ~1.06 B/elem). Decode (1 row) runs the matching on-GPU quant matvec kernel directly out of
1732    // the block format (no CPU dequant, no re-pack) -- the bandwidth lever for memory-bound decode.
1733    // Prefill (>1 row) dequantizes the original `qtensor` to a temporary f32 weight. `dtype` selects
1734    // the kernel; `n`/`k` are the weight dims.
1735    #[cfg(feature = "vulkan")]
1736    VulkanQuant {
1737        qtensor: std::sync::Arc<QTensor>,
1738        wq: std::sync::Arc<crate::VulkanStorage>,
1739        dtype: GgmlDType,
1740        n: usize,
1741        k: usize,
1742    },
1743    // wgpu mirror of VulkanQuant: GGML quantized blocks live in VRAM and decode (1 row) runs the
1744    // matching native-GGML quant matvec WGSL kernel straight out of the block format. Prefill (>1
1745    // row) dequantizes to a temporary f32 weight. `dtype` selects the kernel; `n`/`k` are the dims.
1746    #[cfg(feature = "wgpu")]
1747    WgpuQuant {
1748        qtensor: std::sync::Arc<QTensor>,
1749        wq: std::sync::Arc<crate::WgpuStorage>,
1750        dtype: GgmlDType,
1751        n: usize,
1752        k: usize,
1753    },
1754    // Native ROCm quantized weight: the GGML blocks live in VRAM. Decode (1 row) runs the ONE
1755    // unified on-GPU quant matvec (qmatvec_core<WTYPE>) straight out of the block format; prefill
1756    // (>1 row) runs the ONE unified int8 WMMA GEMM (qmmq_core<WTYPE>) -- both for the full wired
1757    // spread (Q8_0/Q4_0/Q4_K/Q6_K/IQ4_XS/TQ2_0). Unwired types dequantize to a temporary f16
1758    // weight (RDNA matrix-core matmul). `n`/`k` are the weight dims.
1759    #[cfg(feature = "rocm")]
1760    RocmQuant {
1761        qtensor: std::sync::Arc<QTensor>,
1762        wq: std::sync::Arc<crate::RocmStorage>,
1763        dtype: GgmlDType,
1764        n: usize,
1765        k: usize,
1766    },
1767}
1768
1769// Upload `bank` through `upload` into this tensor's own resident `slot` on first call, then hand back
1770// that copy on every later call. The whole point: the H2D copy happens once, not per token.
1771//
1772// The slot lives in the owning QTensor, so a resident bank is reachable only from the bytes it was
1773// built from and is freed with them. A global map keyed by the CPU buffer's (ptr,len) cannot express
1774// that: the key stays live after the tensor drops, and the allocator reuses the address for the next
1775// same-size bank, so a later tensor reads an earlier one's weights. That aliased silently -- Q4_0 and
1776// Q4_K are both 0.5625 bytes/weight, so their banks are byte-identical in LENGTH for a given shape,
1777// and dtype was not part of the key.
1778#[cfg(any(feature = "rocm", feature = "vulkan", feature = "wgpu"))]
1779fn cache_or_upload<S>(
1780    slot: &std::sync::OnceLock<std::sync::Arc<S>>,
1781    bank: &[u8],
1782    upload: impl FnOnce(&[u8]) -> Result<S>,
1783) -> Result<std::sync::Arc<S>> {
1784    if let Some(w) = slot.get() {
1785        return Ok(w.clone());
1786    }
1787    let w = std::sync::Arc::new(upload(bank)?);
1788    // A racing caller may have filled the slot meanwhile; whoever lands first wins and both return
1789    // the same resident bank (the loser's upload drops).
1790    Ok(slot.get_or_init(|| w).clone())
1791}
1792
1793// GGML dtype -> native fused grouped quant-matvec MoE kernel name on the Vulkan backend. ONE source
1794// of truth: the construction gate keeps the [E,n,k] bank quantized iff this returns Some, the
1795// dispatch guard fires on Some, and the branch selects the returned kernel. `None` dtypes fall
1796// through to dequantize. Q4_0/Q8_0/Q4K read the raw (or Q8_0-repacked) GGML bytes; Q6_K reads the
1797// padded 53-u32 super-block layout from `quantize_q6k`.
1798#[cfg(feature = "vulkan")]
1799fn vk_moe_kernel(dt: GgmlDType) -> Option<&'static str> {
1800    match dt {
1801        GgmlDType::Q4_0 => Some("moe_matvec_q4_0"),
1802        GgmlDType::Q8_0 => Some("moe_matvec_q8_0"),
1803        GgmlDType::Q4K => Some("moe_matvec_q4k"),
1804        GgmlDType::Q6K => Some("moe_matvec_q6k"),
1805        _ => None,
1806    }
1807}
1808
1809// (dtype, n, k) -> committed DSL block-reduced MoE .spv. `n,k` are baked into each artifact at dump
1810// time (comptime -> full unroll + magic-multiply divide), so selection is per shape: gate/up (k=2048,
1811// nt=64) and down (k=768, nt=32) for Qwen3-30B-A3B. Shapes without a committed .spv return None and
1812// take the packed `vk_moe_kernel` path -- add a shape by dumping it in `matvec-check dump`. The split
1813// bank (`vulkan_moe_bank_split`) is uploaded iff this returns Some, keeping the two paths orthogonal.
1814#[cfg(feature = "vulkan")]
1815fn vk_moe_blk_kernel(dt: GgmlDType, n: usize, k: usize) -> Option<&'static str> {
1816    // A/B escape hatch: VK_MOE_PACKED forces the packed naive path so the block kernels' end-to-end
1817    // contribution is measurable under identical conditions (no cheating -- show the delta).
1818    if std::env::var_os("VK_MOE_PACKED").is_some() {
1819        return None;
1820    }
1821    match (dt, n, k) {
1822        (GgmlDType::Q4K, 768, 2048) => Some("moe_matvec_q4k_blk_gu"),
1823        (GgmlDType::Q4K, 2048, 768) => Some("moe_matvec_q4k_blk_dn"),
1824        (GgmlDType::Q6K, 2048, 768) => Some("moe_matvec_q6k_blk_dn"),
1825        _ => None,
1826    }
1827}
1828
1829// dp4a (int8 OpSDot) block kernels for the shapes with a committed .spv. Selected before the f32 block
1830// kernel when the device advertises integer dot-product (see has_int_dot8). VK_MOE_DP4A_OFF forces the
1831// f32 path for an A/B. The bool is the kernel's activation-binding contract: whether it binds the
1832// per-32 q8 sums (`xsum`; Q4_K folds dmin against them) or derives its own half-block sums in-register
1833// (Q6_K's −32 fold needs per-16 sums, which per-32 xsum cannot express).
1834// Gated exactly like its sibling vk_moe_blk_kernel above. Both call sites are
1835// inside `#[cfg(feature = "vulkan")]` blocks, so without the feature this compiled
1836// with no callers and clippy correctly called it dead.
1837#[cfg(feature = "vulkan")]
1838fn vk_moe_blk_dp4a_kernel(dt: GgmlDType, n: usize, k: usize) -> Option<(&'static str, bool)> {
1839    if std::env::var_os("VK_MOE_PACKED").is_some() || std::env::var_os("VK_MOE_DP4A_OFF").is_some()
1840    {
1841        return None;
1842    }
1843    match (dt, n, k) {
1844        (GgmlDType::Q4K, 768, 2048) => Some(("moe_matvec_q4k_dp4a_blk_gu", true)),
1845        (GgmlDType::Q4K, 2048, 768) => Some(("moe_matvec_q4k_dp4a_blk_dn", true)),
1846        (GgmlDType::Q6K, 2048, 768) => Some(("moe_matvec_q6k_dp4a_blk_dn", false)),
1847        _ => None,
1848    }
1849}
1850
1851// wgpu twin of `vk_moe_kernel`. The WGSL MoE shaders cover Q4_0/Q8_0/Q4K; Q6_K is Vulkan-only so far.
1852#[cfg(feature = "wgpu")]
1853fn wgpu_moe_kernel(dt: GgmlDType) -> Option<&'static str> {
1854    match dt {
1855        GgmlDType::Q4_0 => Some("moe_matvec_q4_0"),
1856        GgmlDType::Q8_0 => Some("moe_matvec_q8_0"),
1857        GgmlDType::Q4K => Some("moe_matvec_q4k"),
1858        _ => None,
1859    }
1860}
1861
1862/// Per-expert quantized MoE matmul, shared by the resident and disk-streaming paths. Groups the
1863/// routed slots by expert id, and for each active expert builds a `QTensor` from `make_storage`
1864/// (resident slice or streamed slab), runs the native quantized matmul on just that expert's tokens,
1865/// and scatters the result back. The ONLY difference between resident and streaming is the closure.
1866fn moe_grouped_per_expert(
1867    x: &Tensor,
1868    ids: &Tensor,
1869    n: usize,
1870    k: usize,
1871    mut make_storage: impl FnMut(u32, &Device) -> Result<QStorage>,
1872) -> Result<Tensor> {
1873    use crate::Module; // brings QMatMul::forward into scope
1874    use std::collections::HashMap;
1875    use std::sync::Arc;
1876    let device = x.device();
1877    let out_dtype = x.dtype();
1878    let (t, topk) = ids.dims2()?;
1879    let s = x.dim(1)?; // 1 (gate/up: shared input) or topk (down: per-slot)
1880    let x_exp = if s == topk {
1881        x.clone()
1882    } else {
1883        x.broadcast_as((t, topk, k))?
1884    };
1885    let x_flat = x_exp
1886        .reshape((t * topk, k))?
1887        .to_dtype(DType::F32)?
1888        .contiguous()?;
1889    let ids_flat = ids.reshape((t * topk,))?.to_dtype(DType::U32)?;
1890    let ids_vec = ids_flat.to_vec1::<u32>()?;
1891    let mut groups: HashMap<u32, Vec<u32>> = HashMap::new();
1892    for (slot, eid) in ids_vec.iter().enumerate() {
1893        groups.entry(*eid).or_default().push(slot as u32);
1894    }
1895    let mut out_flat = Tensor::zeros((t * topk, n), DType::F32, device)?;
1896    for (eid, slots) in groups.into_iter() {
1897        let qs = make_storage(eid, device)?;
1898        let shape: crate::Shape = (n, k).into();
1899        let w_e = QTensor::make(qs, shape);
1900        let qm = QMatMul::from_arc(Arc::new(w_e))?;
1901        let m = slots.len();
1902        let idx = Tensor::from_vec(slots, (m,), device)?;
1903        let x_e = x_flat.index_select(&idx, 0)?; // [m, k]
1904        let y_e = qm.forward(&x_e)?.to_dtype(DType::F32)?; // [m, n]
1905        out_flat = out_flat.index_add(&idx, &y_e, 0)?;
1906    }
1907    out_flat.reshape((t, topk, n))?.to_dtype(out_dtype)
1908}
1909
1910/// FUSED MoE expert-combine: `out[i,j] = sum_e scores[i,e] * ys[i,e,j]`, reducing the per-expert
1911/// outputs `ys` [t, topk, n] by the router weights `scores` [t, topk] into [t, n]. On ROCm this is
1912/// ONE fused kernel (`RocmDevice::moe_combine`) instead of `ys.broadcast_mul(scores).sum(Minus2)`,
1913/// which cast ys -> f32, wrote a [t,topk,n] f32 product temp, and ran an 8-wide strided reduce.
1914/// Other backends keep the generic broadcast-mul + sum.
1915#[cfg_attr(not(feature = "rocm"), allow(unused_variables))]
1916pub fn moe_combine(ys: &Tensor, scores: &Tensor) -> Result<Tensor> {
1917    let (t, topk, n) = ys.dims3()?;
1918    #[cfg(feature = "rocm")]
1919    if let Device::Rocm(dev) = ys.device() {
1920        let ys_c = ys.contiguous()?;
1921        let scores_c = scores.to_dtype(DType::F32)?.contiguous()?;
1922        let (ys_store, _) = ys_c.storage_and_layout();
1923        let yr = match &*ys_store {
1924            Storage::Rocm(r) => r,
1925            _ => crate::bail!("moe_combine: ys not on rocm after contiguous()"),
1926        };
1927        let (sc_store, _) = scores_c.storage_and_layout();
1928        let sr = match &*sc_store {
1929            Storage::Rocm(r) => r,
1930            _ => crate::bail!("moe_combine: scores not on rocm after contiguous()"),
1931        };
1932        let out = dev.moe_combine(yr, sr, t, topk, n)?;
1933        return Ok(crate::tensor::from_storage(
1934            Storage::Rocm(out),
1935            (t, n),
1936            crate::op::BackpropOp::none(),
1937            false,
1938        ));
1939    }
1940    // scores come from moe_route in f32; ys carries the model compute dtype, which on a backend
1941    // without a native moe_combine kernel (Metal) is bf16/f16 -> a raw broadcast_mul would hit a
1942    // dtype mismatch. Accumulate in f32 (as the rocm/cuda kernels do), then restore ys's dtype.
1943    // No-op for the f32 path, so CPU/CUDA stay byte-identical.
1944    let out_dtype = ys.dtype();
1945    ys.to_dtype(DType::F32)?
1946        .broadcast_mul(&scores.to_dtype(DType::F32)?.unsqueeze(D::Minus1)?)?
1947        .sum(D::Minus2)?
1948        .to_dtype(out_dtype)
1949}
1950
1951/// Fused MoE router. Reduces the F32 router logits [ntok, n_experts] to the topk selected expert
1952/// ids [ntok, topk] (descending logit) and their softmax weights [ntok, topk]; `norm` renormalizes
1953/// the topk weights to sum 1 (norm_topk_prob). On ROCm this is ONE `moe_route` kernel replacing the
1954/// softmax->sort->narrow->sum->div chain (~6 launches/layer); elsewhere it is that chain via ml ops.
1955#[cfg_attr(not(feature = "rocm"), allow(unused_variables))]
1956pub fn moe_route(logits: &Tensor, topk: usize, norm: bool) -> Result<(Tensor, Tensor)> {
1957    let (ntok, n_experts) = logits.dims2()?;
1958    #[cfg(feature = "rocm")]
1959    if let Device::Rocm(dev) = logits.device() {
1960        let logits_c = logits.to_dtype(DType::F32)?.contiguous()?;
1961        let (lg_store, _) = logits_c.storage_and_layout();
1962        let lr = match &*lg_store {
1963            Storage::Rocm(r) => r,
1964            _ => crate::bail!("moe_route: logits not on rocm after contiguous()"),
1965        };
1966        let (ids, w) = dev.moe_route(lr, ntok, n_experts, topk, norm)?;
1967        let ids_t = crate::tensor::from_storage(
1968            Storage::Rocm(ids),
1969            (ntok, topk),
1970            crate::op::BackpropOp::none(),
1971            false,
1972        );
1973        let w_t = crate::tensor::from_storage(
1974            Storage::Rocm(w),
1975            (ntok, topk),
1976            crate::op::BackpropOp::none(),
1977            false,
1978        );
1979        return Ok((ids_t, w_t));
1980    }
1981    #[cfg(feature = "cuda")]
1982    if let Device::Cuda(cdev) = logits.device() {
1983        if n_experts <= 256 && topk <= 32 {
1984            let logits_c = logits.to_dtype(DType::F32)?.contiguous()?;
1985            let (lg_store, _) = logits_c.storage_and_layout();
1986            let lr = match &*lg_store {
1987                Storage::Cuda(c) => c,
1988                _ => crate::bail!("moe_route: logits not on cuda after contiguous()"),
1989            };
1990            let lview = lr.as_cuda_slice::<f32>()?.slice(0..);
1991            let (ids, w) = cuda::moe_route(&lview, ntok, n_experts, topk, norm, cdev)?;
1992            let ids_t = crate::tensor::from_storage(
1993                Storage::Cuda(ids),
1994                (ntok, topk),
1995                crate::op::BackpropOp::none(),
1996                false,
1997            );
1998            let w_t = crate::tensor::from_storage(
1999                Storage::Cuda(w),
2000                (ntok, topk),
2001                crate::op::BackpropOp::none(),
2002                false,
2003            );
2004            return Ok((ids_t, w_t));
2005        }
2006    }
2007    // Fused Vulkan router (one workgroup/token, softmax + top-k in shared mem) for the committed .spv
2008    // shape (E=128, top-8) with norm_topk_prob; replaces the generic softmax+sort+gather op-chain and
2009    // its per-op layout copies. Other shapes / norm=false fall through to the generic path below.
2010    #[cfg(feature = "vulkan")]
2011    if let Device::Vulkan(vdev) = logits.device() {
2012        if norm && n_experts == 128 && topk == 8 {
2013            let logits_c = logits.to_dtype(DType::F32)?.contiguous()?;
2014            let (lg_store, _) = logits_c.storage_and_layout();
2015            let lv = match &*lg_store {
2016                Storage::Vulkan(v) => v,
2017                _ => crate::bail!("moe_route: logits not on vulkan after contiguous()"),
2018            };
2019            let (ids, w) = vdev.moe_route_vk(lv, ntok, n_experts, topk)?;
2020            let ids_t = crate::tensor::from_storage(
2021                Storage::Vulkan(ids),
2022                (ntok, topk),
2023                crate::op::BackpropOp::none(),
2024                false,
2025            );
2026            let w_t = crate::tensor::from_storage(
2027                Storage::Vulkan(w),
2028                (ntok, topk),
2029                crate::op::BackpropOp::none(),
2030                false,
2031            );
2032            return Ok((ids_t, w_t));
2033        }
2034    }
2035    let lf = logits.to_dtype(DType::F32)?;
2036    let mx = lf.max_keepdim(D::Minus1)?;
2037    let e = lf.broadcast_sub(&mx)?.exp()?;
2038    let z = e.sum_keepdim(D::Minus1)?;
2039    let p = e.broadcast_div(&z)?;
2040    let (sv, si) = p.sort_last_dim(false)?;
2041    let ids = si.narrow(D::Minus1, 0, topk)?.contiguous()?;
2042    let mut w = sv.narrow(D::Minus1, 0, topk)?.contiguous()?;
2043    if norm {
2044        w = w.broadcast_div(&w.sum_keepdim(D::Minus1)?)?;
2045    }
2046    Ok((ids, w))
2047}
2048
2049/// Fused MoE gate+up projections. Both expert banks consume the SAME routed token `x` [t,1,k], so the
2050/// shared input is broadcast + quantized ONCE and matvec'd against both banks (vs once per bank,
2051/// re-materializing + re-quantizing the identical activation). Returns the raw (gate_out, up_out)
2052/// [t,topk,n]; the caller applies silu(gate)*up. Each output is bit-identical to the unfused
2053/// `indexed_moe_forward` because the q8_1 activation is deterministic in `x`. ROCm decode/matvec path
2054/// only (the prefill qmmq path keeps its own per-bank quantize); every other case runs the two
2055/// unfused forwards.
2056pub fn moe_gate_up(
2057    x: &Tensor,
2058    ids: &Tensor,
2059    gate: &QMatMul,
2060    up: &QMatMul,
2061) -> Result<(Tensor, Tensor)> {
2062    #[cfg(feature = "rocm")]
2063    {
2064        if let (QMatMul::QTensor(gq), QMatMul::QTensor(uq)) = (gate, up) {
2065            if let (QStorage::Rocm(_, dev), QStorage::Rocm(..)) = (&gq.storage, &uq.storage) {
2066                let dt = gq.storage.dtype();
2067                if dt == uq.storage.dtype() {
2068                    if let Some(qt) = crate::RocmQuantType::from_ggml(dt) {
2069                        let (_e, n, k) = gq.shape().dims3()?;
2070                        let (t, topk) = ids.dims2()?;
2071                        let use_qmmq = t > 1 && qt.qmmq_capable();
2072                        if x.dim(1)? == 1 && !use_qmmq {
2073                            let nrows = t * topk;
2074                            let x_exp = x.broadcast_as((t, topk, k))?;
2075                            let x_flat = match x_exp.dtype() {
2076                                DType::BF16 | DType::F16 => {
2077                                    x_exp.reshape((nrows, k))?.contiguous()?
2078                                }
2079                                DType::F32 if qt.dp4a_active() => {
2080                                    x_exp.reshape((nrows, k))?.contiguous()?
2081                                }
2082                                _ => x_exp
2083                                    .reshape((nrows, k))?
2084                                    .to_dtype(DType::F16)?
2085                                    .contiguous()?,
2086                            };
2087                            let out_dtype = x.dtype();
2088                            let ids_u32 =
2089                                ids.reshape((nrows,))?.to_dtype(DType::U32)?.contiguous()?;
2090                            let gwb = gq.rocm_moe_bank(dev)?;
2091                            let uwb = uq.rocm_moe_bank(dev)?;
2092                            let (xstore, _) = x_flat.storage_and_layout();
2093                            let xr = match &*xstore {
2094                                Storage::Rocm(r) => r,
2095                                _ => crate::bail!("moe_gate_up: x not on rocm after contiguous()"),
2096                            };
2097                            let (idstore, _) = ids_u32.storage_and_layout();
2098                            let idr = match &*idstore {
2099                                Storage::Rocm(r) => r,
2100                                _ => crate::bail!("moe_gate_up: ids not on rocm"),
2101                            };
2102                            let (gy, uy) = dev.moe_matvec_pair(
2103                                qt,
2104                                gwb.as_ref(),
2105                                uwb.as_ref(),
2106                                xr,
2107                                idr,
2108                                nrows,
2109                                n,
2110                                k,
2111                            )?;
2112                            let g = crate::tensor::from_storage(
2113                                Storage::Rocm(gy),
2114                                (nrows, n),
2115                                crate::op::BackpropOp::none(),
2116                                false,
2117                            )
2118                            .reshape((t, topk, n))?
2119                            .to_dtype(out_dtype)?;
2120                            let u = crate::tensor::from_storage(
2121                                Storage::Rocm(uy),
2122                                (nrows, n),
2123                                crate::op::BackpropOp::none(),
2124                                false,
2125                            )
2126                            .reshape((t, topk, n))?
2127                            .to_dtype(out_dtype)?;
2128                            return Ok((g, u));
2129                        }
2130                    }
2131                }
2132            }
2133        }
2134    }
2135    // Vulkan dp4a twin of the ROCm block above: gate and up contract the SAME routed token, so the
2136    // q8 activation quantize is hoisted out and both matvecs dispatch against one copy. Falling
2137    // through to two `indexed_moe_forward` calls re-derives it per matvec -- byte-identical work,
2138    // twice. Only the quantize is shared; the dispatch is `moe_matvec_blk_dp4a_pre_gpu` either way.
2139    // A/B escape hatch, mirroring VK_MOE_PACKED / VK_MOE_DP4A_OFF: VK_MOE_GU_FUSE_OFF falls through to
2140    // the two-call path so the shared quantize can be measured against re-quantizing per matvec.
2141    #[cfg(feature = "vulkan")]
2142    if std::env::var_os("VK_MOE_GU_FUSE_OFF").is_none() {
2143        if let (QMatMul::QTensor(gq), QMatMul::QTensor(uq)) = (gate, up) {
2144            if let (QStorage::Vulkan(_, dev), QStorage::Vulkan(..)) = (&gq.storage, &uq.storage) {
2145                let dt = gq.storage.dtype();
2146                if dt == uq.storage.dtype() && dev.has_int_dot8() {
2147                    let (e_cnt, n, k) = gq.shape().dims3()?;
2148                    // gate/up share one input row per token; the per-slot (down) shape is not this path.
2149                    if uq.shape().dims3()? == (e_cnt, n, k) && x.dim(1)? == 1 {
2150                        if let Some((blk, with_xsum)) = vk_moe_blk_dp4a_kernel(dt, n, k) {
2151                            let (t, topk) = ids.dims2()?;
2152                            let nrows = t * topk;
2153                            let x_flat = x
2154                                .broadcast_as((t, topk, k))?
2155                                .reshape((nrows, k))?
2156                                .to_dtype(DType::F32)?
2157                                .contiguous()?;
2158                            let ids_u32 =
2159                                ids.reshape((nrows,))?.to_dtype(DType::U32)?.contiguous()?;
2160                            let (xstore, _) = x_flat.storage_and_layout();
2161                            let xv = match &*xstore {
2162                                Storage::Vulkan(v) => v,
2163                                _ => {
2164                                    crate::bail!("moe_gate_up: x not on vulkan after contiguous()")
2165                                }
2166                            };
2167                            let (idstore, _) = ids_u32.storage_and_layout();
2168                            let idv = match &*idstore {
2169                                Storage::Vulkan(v) => v,
2170                                _ => crate::bail!("moe_gate_up: ids not on vulkan"),
2171                            };
2172                            // The one quantize both projections read.
2173                            let (xq, xs, xsum) = dev.quantize_act_q8(xv, nrows, k)?;
2174                            let gbank = gq.vulkan_moe_bank_split(dev, e_cnt, n, k)?;
2175                            let ubank = uq.vulkan_moe_bank_split(dev, e_cnt, n, k)?;
2176                            let out_dtype = x.dtype();
2177                            // PREFILL (t > 1): expert-grouped MMQ on the matrix cores, the same
2178                            // t > 1 split `indexed_moe_forward` makes. Gate and up share the routing,
2179                            // so they also share ONE grouping pass. DECODE (t == 1) keeps the matvec.
2180                            let (gy, uy) = if t > 1
2181                                && dt == GgmlDType::Q4K
2182                                && std::env::var_os("VK_MOE_PREFILL_GEMM_OFF").is_none()
2183                            {
2184                                let (counts, rows) =
2185                                    dev.moe_expert_rows_vk(idv, nrows, e_cnt, t)?;
2186                                let gy = dev.mmq_q4k_id_pre_gpu(
2187                                    gbank.as_ref(),
2188                                    &xq,
2189                                    &xs,
2190                                    &xsum,
2191                                    &rows,
2192                                    &counts,
2193                                    nrows,
2194                                    e_cnt,
2195                                    t,
2196                                    n,
2197                                    k,
2198                                )?;
2199                                let uy = dev.mmq_q4k_id_pre_gpu(
2200                                    ubank.as_ref(),
2201                                    &xq,
2202                                    &xs,
2203                                    &xsum,
2204                                    &rows,
2205                                    &counts,
2206                                    nrows,
2207                                    e_cnt,
2208                                    t,
2209                                    n,
2210                                    k,
2211                                )?;
2212                                (gy, uy)
2213                            } else {
2214                                let gy = dev.moe_matvec_blk_dp4a_pre_gpu(
2215                                    blk,
2216                                    with_xsum,
2217                                    gbank.as_ref(),
2218                                    &xq,
2219                                    &xs,
2220                                    &xsum,
2221                                    idv,
2222                                    nrows,
2223                                    n,
2224                                )?;
2225                                let uy = dev.moe_matvec_blk_dp4a_pre_gpu(
2226                                    blk,
2227                                    with_xsum,
2228                                    ubank.as_ref(),
2229                                    &xq,
2230                                    &xs,
2231                                    &xsum,
2232                                    idv,
2233                                    nrows,
2234                                    n,
2235                                )?;
2236                                (gy, uy)
2237                            };
2238                            let shape = |o| -> Result<Tensor> {
2239                                crate::tensor::from_storage(
2240                                    Storage::Vulkan(o),
2241                                    (nrows, n),
2242                                    crate::op::BackpropOp::none(),
2243                                    false,
2244                                )
2245                                .reshape((t, topk, n))?
2246                                .to_dtype(out_dtype)
2247                            };
2248                            return Ok((shape(gy)?, shape(uy)?));
2249                        }
2250                    }
2251                }
2252            }
2253        }
2254    }
2255    Ok((
2256        gate.indexed_moe_forward(x, ids)?,
2257        up.indexed_moe_forward(x, ids)?,
2258    ))
2259}
2260
2261impl QMatMul {
2262    pub fn from_arc(qtensor: std::sync::Arc<QTensor>) -> Result<Self> {
2263        // Native Vulkan quantized path: keep the GGML quantized blocks in VRAM and run the matching
2264        // on-GPU quant matvec for decode, instead of dequantizing the whole model to f32 (4x the
2265        // decode bandwidth). The kernel reads the GGML block format straight from the uploaded bytes
2266        // -- no CPU dequant, no re-pack -- so this is exact w.r.t. the CPU reference. Q4_0/Q4_K need
2267        // k a multiple of their block (32 / 256); Q8_0 needs a multiple of 32.
2268        #[cfg(feature = "vulkan")]
2269        {
2270            let dt = qtensor.dtype();
2271            let native_vk = matches!(
2272                dt,
2273                GgmlDType::Q4_0
2274                    | GgmlDType::Q8_0
2275                    | GgmlDType::Q4K
2276                    | GgmlDType::Q5K
2277                    | GgmlDType::Q6K
2278                    | GgmlDType::Q2K
2279                    | GgmlDType::Q3K
2280                    | GgmlDType::IQ4_XS
2281                    | GgmlDType::IQ4_NL
2282                    | GgmlDType::TQ2_0
2283                    | GgmlDType::IQ2_XXS
2284                    | GgmlDType::IQ2_S
2285                    | GgmlDType::IQ3_XXS
2286                    | GgmlDType::IQ3_S
2287                    | GgmlDType::IQ1_S
2288                    | GgmlDType::IQ1_M
2289                    | GgmlDType::IQ2_XS
2290            );
2291            if native_vk {
2292                if let Device::Vulkan(d) = qtensor.device() {
2293                    if let Ok((n, k)) = qtensor.shape().dims2() {
2294                        let blk = dt.block_size();
2295                        if k % blk == 0 {
2296                            let bytes = qtensor.data()?;
2297                            // Q6_K (210 B) and Q3_K (110 B) blocks are not u32-aligned; their shaders
2298                            // read a padded u32 stride, so repack on upload. Q8_0 repacks to the
2299                            // 9-u32/block layout that BOTH its decode (mul_mat_vec_q8) and prefill GEMM
2300                            // (mul_mat_q8) read -- ONE layout, so decode + prefill + MoE all agree
2301                            // (mirrors how the MoE bank repacks Q8_0). Every other native type's
2302                            // shaders byte-address the raw GGML bytes directly.
2303                            let wq = match dt {
2304                                GgmlDType::Q6K => d.quantize_q6k(&bytes, n, k)?,
2305                                GgmlDType::Q3K => d.quantize_q3k(&bytes, n, k)?,
2306                                GgmlDType::Q8_0 => d.quantize_q8_blocks(&bytes, n, k)?,
2307                                GgmlDType::IQ2_XXS => d.quantize_iq2xxs(&bytes, n, k)?,
2308                                GgmlDType::IQ2_XS => d.quantize_iq2xs(&bytes, n, k)?,
2309                                GgmlDType::IQ1_M => d.quantize_iq1m(&bytes, n, k)?,
2310                                GgmlDType::IQ1_S => d.quantize_iq1s(&bytes, n, k)?,
2311                                GgmlDType::IQ3_S => d.quantize_iq3s(&bytes, n, k)?,
2312                                GgmlDType::IQ3_XXS => d.quantize_iq3xxs(&bytes, n, k)?,
2313                                GgmlDType::IQ2_S => d.quantize_iq2s(&bytes, n, k)?,
2314                                _ => d.upload_qweight(&bytes)?,
2315                            };
2316                            return Ok(Self::VulkanQuant {
2317                                qtensor,
2318                                wq: std::sync::Arc::new(wq),
2319                                dtype: dt,
2320                                n,
2321                                k,
2322                            });
2323                        }
2324                    }
2325                }
2326            }
2327        }
2328        // Native wgpu quantized path: same idea as Vulkan. Ships the Q4_0/Q8_0/Q4_K WGSL matvec
2329        // kernels; other dtypes fall through to the dequantize path below.
2330        #[cfg(feature = "wgpu")]
2331        {
2332            let dt = qtensor.dtype();
2333            let native_wgpu = matches!(dt, GgmlDType::Q4_0 | GgmlDType::Q8_0 | GgmlDType::Q4K);
2334            if native_wgpu {
2335                if let Device::Wgpu(d) = qtensor.device() {
2336                    if let Ok((n, k)) = qtensor.shape().dims2() {
2337                        let blk = dt.block_size();
2338                        if k % blk == 0 {
2339                            let bytes = qtensor.data()?;
2340                            let wq = d.upload_qweight(&bytes)?;
2341                            return Ok(Self::WgpuQuant {
2342                                qtensor,
2343                                wq: std::sync::Arc::new(wq),
2344                                dtype: dt,
2345                                n,
2346                                k,
2347                            });
2348                        }
2349                    }
2350                }
2351            }
2352        }
2353        // Native ROCm quantized path: keep the GGML blocks in VRAM and run the ONE unified on-GPU
2354        // quant decode core (qmatvec_core<WTYPE>; Q8_0+Q4_0 also have the int8 WMMA prefill gemm),
2355        // instead of dequantizing the whole model to dense f16 (2x+ the decode bandwidth). Reads the
2356        // block format straight from the uploaded bytes -- exact w.r.t. the CPU reference. The wired
2357        // set is exactly RocmQuantType::from_ggml; k must be a multiple of that type's block size.
2358        #[cfg(feature = "rocm")]
2359        {
2360            let dt = qtensor.dtype();
2361            // Decode AND prefill native iff the unified core has this type wired (one enum row in
2362            // RocmQuantType): decode rides qmatvec_core<WTYPE>, prefill (rows>1) rides the int8 WMMA
2363            // qmmq_core<WTYPE>. Both cover the SAME wired spread; adding a type is one enum row + the
2364            // in-kernel decode, no per-quant kernel. Unwired types dequantize-to-f16 in forward().
2365            if let Some(qt) = crate::RocmQuantType::from_ggml(dt) {
2366                if let Device::Rocm(d) = qtensor.device() {
2367                    if let Ok((n, k)) = qtensor.shape().dims2() {
2368                        let blk_ok = k % qt.block_elems() == 0;
2369                        if blk_ok {
2370                            use crate::backend::BackendDevice;
2371                            let bytes = qtensor.data()?;
2372                            let wq = d.storage_from_slice(bytes.as_ref())?;
2373                            return Ok(Self::RocmQuant {
2374                                qtensor,
2375                                wq: std::sync::Arc::new(wq),
2376                                dtype: dt,
2377                                n,
2378                                k,
2379                            });
2380                        }
2381                    }
2382                }
2383            }
2384        }
2385        // ROCm MoE bank: a 3D [E,n,k] expert bank of a wired quant type stays QUANTIZED (kept as
2386        // QTensor), so `indexed_moe_forward` runs each routed expert through the ONE unified
2387        // qmatvec_core (no per-expert kernel) instead of dequantizing the whole bank to dense f16
2388        // (which for a 30B-A3B model is many GB of resident f16 AND has no indexed_moe path). The 2D
2389        // RocmQuant decode/prefill path above already handles ordinary weights; this is the MoE case.
2390        #[cfg(feature = "rocm")]
2391        {
2392            if qtensor.device().is_rocm()
2393                && qtensor.shape().dims().len() == 3
2394                && crate::RocmQuantType::from_ggml(qtensor.dtype()).is_some()
2395            {
2396                return Ok(Self::QTensor(qtensor));
2397            }
2398        }
2399        // Vulkan/wgpu MoE bank: a 3D [E,n,k] expert bank whose dtype has a native fused grouped
2400        // quant-matvec kernel stays QUANTIZED (kept as QTensor), so `indexed_moe_forward` gathers
2401        // the routed experts on the GPU straight from the resident bank -- the twin of the ROCm
2402        // case above. Dequantizing here instead explodes the bank to dense f32 in VRAM (30B-A3B ->
2403        // 100+ GB) AND lands on the generic path, whose Vulkan/wgpu index_add is unwired.
2404        #[cfg(feature = "vulkan")]
2405        {
2406            if qtensor.device().is_vulkan()
2407                && qtensor.shape().dims().len() == 3
2408                && vk_moe_kernel(qtensor.dtype()).is_some()
2409            {
2410                return Ok(Self::QTensor(qtensor));
2411            }
2412        }
2413        #[cfg(feature = "wgpu")]
2414        {
2415            if qtensor.device().is_wgpu()
2416                && qtensor.shape().dims().len() == 3
2417                && wgpu_moe_kernel(qtensor.dtype()).is_some()
2418            {
2419                return Ok(Self::QTensor(qtensor));
2420            }
2421        }
2422        let dequantize = match qtensor.dtype() {
2423            GgmlDType::F32 | GgmlDType::F16 | GgmlDType::BF16 | GgmlDType::I32 => true,
2424            // The Vulkan/wgpu/ROCm backends have no generic native quantized matmul, so dequantize
2425            // to f32 here (once, at construction) and run the regular f32 GPU matmul.
2426            _ => {
2427                qtensor.device().is_vulkan()
2428                    || qtensor.device().is_wgpu()
2429                    || qtensor.device().is_rocm()
2430            }
2431        };
2432        let t = if dequantize {
2433            // ROCm: dequantize to f16 so the matmul hits RDNA3.5 matrix cores (WMMA). Dense f32
2434            // (sgemm) has no matrix-core path on RDNA and runs ~an order of magnitude slower, and
2435            // f16 also halves the resident weight memory.
2436            if qtensor.device().is_rocm() {
2437                Self::TensorF16(qtensor.dequantize_f16(&qtensor.device())?)
2438            } else {
2439                Self::Tensor(qtensor.dequantize(&qtensor.device())?)
2440            }
2441        } else {
2442            Self::QTensor(qtensor)
2443        };
2444        Ok(t)
2445    }
2446
2447    pub fn from_qtensor(qtensor: QTensor) -> Result<Self> {
2448        Self::from_arc(std::sync::Arc::new(qtensor))
2449    }
2450
2451    pub fn dequantize_f16(&self) -> Result<Tensor> {
2452        match self {
2453            Self::QTensor(t) => t.dequantize_f16(&t.device()),
2454            Self::Tensor(t) => t.to_dtype(DType::F16),
2455            Self::TensorF16(t) => Ok(t.clone()),
2456            #[cfg(feature = "rocm")]
2457            Self::RocmQuant { qtensor, .. } => qtensor.dequantize_f16(&qtensor.device()),
2458            #[cfg(feature = "vulkan")]
2459            Self::VulkanQuant { qtensor, .. } => qtensor.dequantize_f16(&qtensor.device()),
2460            #[cfg(feature = "wgpu")]
2461            Self::WgpuQuant { qtensor, .. } => qtensor.dequantize_f16(&qtensor.device()),
2462        }
2463    }
2464
2465    pub fn forward_via_f16(&self, xs: &Tensor) -> Result<Tensor> {
2466        let w = self.dequantize_f16()?;
2467        let in_dtype = xs.dtype();
2468        let w = match *xs.dims() {
2469            [b1, b2, _, _] => w.broadcast_left((b1, b2))?.t()?,
2470            [bsize, _, _] => w.broadcast_left(bsize)?.t()?,
2471            _ => w.t()?,
2472        };
2473        xs.to_dtype(DType::F16)?.matmul(&w)?.to_dtype(in_dtype)
2474    }
2475
2476    pub fn indexed_moe_forward(&self, x: &Tensor, ids: &Tensor) -> Result<Tensor> {
2477        match self {
2478            Self::QTensor(t) => t.indexed_moe_forward(x, ids),
2479            // Resident-bank MoE: `wq` already holds the [E,n,k] GGML blocks in VRAM (uploaded at
2480            // load), so route straight to the batched on-GPU quant matvec. Delegating to `qtensor`
2481            // (CPU-side) instead drops to the generic fallback that re-uploads every routed expert
2482            // every token -- the 20-50x decode cliff. `qtensor` is read only for its shape.
2483            #[cfg(feature = "rocm")]
2484            Self::RocmQuant {
2485                qtensor, wq, dtype, ..
2486            } if crate::RocmQuantType::from_ggml(*dtype).is_some() => {
2487                let qt = crate::RocmQuantType::from_ggml(*dtype).unwrap();
2488                let wbank = wq.as_ref();
2489                // e_cnt unused: ids stay on-device, router guarantees the bound (no host check).
2490                let (_e_cnt, n, k) = qtensor.shape().dims3()?;
2491                let (t, topk) = ids.dims2()?;
2492                let s = x.dim(1)?; // 1 (gate/up: shared input) or topk (down: per-slot)
2493                let x_exp = if s == topk {
2494                    x.clone()
2495                } else {
2496                    x.broadcast_as((t, topk, k))?
2497                };
2498                let nrows = t * topk;
2499                // PREFILL (t>1, never graph-captured) routes to the FUSED expert-grouped WMMA GEMM
2500                // (`moe_qmmq_quant`), which needs f16 activations; DECODE (t==1) stays on the
2501                // capture-clean dp4a/scalar matvec, which takes bf16/f16 natively.
2502                // Decode-only types (no qmmq kernel) ride the per-slot matvec core for prefill too
2503                // (correct at any token count). ONE predicate gates every prefill site.
2504                let use_qmmq = t > 1 && qt.qmmq_capable();
2505                let x_flat = match x_exp.dtype() {
2506                    // qmmq quantizes f16/f32 activations natively, so keep the model's dtype and skip
2507                    // the f32->f16 cast (a 16.7M-elem read+write per gate/up). Other dtypes (bf16 with
2508                    // a symmetric expert type) still cast to f16.
2509                    DType::F16 | DType::F32 if use_qmmq => {
2510                        x_exp.reshape((nrows, k))?.contiguous()?
2511                    }
2512                    _ if use_qmmq => x_exp
2513                        .reshape((nrows, k))?
2514                        .to_dtype(DType::F16)?
2515                        .contiguous()?,
2516                    DType::BF16 | DType::F16 => x_exp.reshape((nrows, k))?.contiguous()?,
2517                    // DECODE f32-native dp4a: keep F32 routed activation F32 end-to-end (matvec stores
2518                    // F32), eliding the cast pair around each expert matvec. See QStorage twin above.
2519                    DType::F32 if qt.dp4a_active() => x_exp.reshape((nrows, k))?.contiguous()?,
2520                    _ => x_exp
2521                        .reshape((nrows, k))?
2522                        .to_dtype(DType::F16)?
2523                        .contiguous()?,
2524                };
2525                let out_dtype = x.dtype();
2526                // Keep router ids ON the GPU for EVERY wired quant type: the batched kernels index
2527                // experts on-device, so there is no per-call `to_vec1` host round-trip. That DtoH
2528                // sync (3 per layer x 48 layers per token) was both the dominant decode stall on WSL
2529                // AND the HIP-graph capture breaker. Router top-k guarantees 0 <= id < e_cnt.
2530                let ids_u32 = ids
2531                    .reshape((nrows,))?
2532                    .to_dtype(crate::DType::U32)?
2533                    .contiguous()?;
2534                let (xstore, _) = x_flat.storage_and_layout();
2535                let xr = match &*xstore {
2536                    crate::Storage::Rocm(r) => r,
2537                    _ => crate::bail!("rocm MoE: x not on rocm after contiguous()"),
2538                };
2539                let (idstore, _) = ids_u32.storage_and_layout();
2540                let idr = match &*idstore {
2541                    crate::Storage::Rocm(r) => r,
2542                    _ => crate::bail!("rocm MoE: ids not on rocm"),
2543                };
2544                let y = if use_qmmq {
2545                    wbank
2546                        .device
2547                        .moe_qmmq_quant(qt, wbank, xr, idr, nrows, n, k)?
2548                } else {
2549                    wbank
2550                        .device
2551                        .moe_matvec_quant(qt, wbank, xr, idr, nrows, n, k)?
2552                };
2553                let out = crate::tensor::from_storage(
2554                    crate::Storage::Rocm(y),
2555                    (nrows, n),
2556                    crate::op::BackpropOp::none(),
2557                    false,
2558                );
2559                out.reshape((t, topk, n))?.to_dtype(out_dtype)
2560            }
2561            // Unwired ROCm quant dtypes (no on-GPU quant matvec): CPU per-expert fallback.
2562            #[cfg(feature = "rocm")]
2563            Self::RocmQuant { qtensor, .. } => qtensor.indexed_moe_forward(x, ids),
2564            #[cfg(feature = "vulkan")]
2565            Self::VulkanQuant { qtensor, .. } => qtensor.indexed_moe_forward(x, ids),
2566            #[cfg(feature = "wgpu")]
2567            Self::WgpuQuant { qtensor, .. } => qtensor.indexed_moe_forward(x, ids),
2568            _ => {
2569                panic!("Not implemented!")
2570            }
2571        }
2572    }
2573
2574    pub fn embedding(&self, ids: &Tensor) -> Result<Tensor> {
2575        match self {
2576            Self::QTensor(t) => t.embedding(ids),
2577            #[cfg(feature = "vulkan")]
2578            Self::VulkanQuant { qtensor, .. } => qtensor.embedding(ids),
2579            #[cfg(feature = "wgpu")]
2580            Self::WgpuQuant { qtensor, .. } => qtensor.embedding(ids),
2581            #[cfg(feature = "rocm")]
2582            Self::RocmQuant { qtensor, .. } => qtensor.embedding(ids),
2583            Self::Tensor(w) | Self::TensorF16(w) => {
2584                let mut final_dims = ids.dims().to_vec();
2585                final_dims.push(w.dim(D::Minus1)?);
2586                let ids = ids.to_device(w.device())?.flatten_all()?;
2587                w.index_select(&ids, 0)?.reshape(final_dims)
2588            }
2589        }
2590    }
2591}
2592
2593impl QTensor {
2594    /// Fused m==1 matmul over same-dtype tensors sharing one lhs (e.g. qkv or gate+up in
2595    /// decode): one lhs quantization and one parallel region. None when unsupported.
2596    pub fn gemv_fused_shared_lhs(ts: &[&Self], lhs: &Tensor) -> Result<Option<Vec<Tensor>>> {
2597        #[cfg(target_arch = "aarch64")]
2598        {
2599            if ts.is_empty() || !lhs.device().is_cpu() || lhs.dtype() != crate::DType::F32 {
2600                return Ok(None);
2601            }
2602            if !lhs.is_contiguous() {
2603                return Ok(None);
2604            }
2605            let dims = lhs.dims();
2606            let Some((&k, batch)) = dims.split_last() else {
2607                return Ok(None);
2608            };
2609            if batch.iter().product::<usize>() != 1 {
2610                return Ok(None);
2611            }
2612            let mut ns = Vec::with_capacity(ts.len());
2613            let mut parts: Vec<(&dyn QuantizedType, &repack::PackedCache)> =
2614                Vec::with_capacity(ts.len());
2615            for t in ts {
2616                let (n, tk) = t.shape.dims2()?;
2617                if tk != k {
2618                    return Ok(None);
2619                }
2620                let QStorage::Cpu(s) = &t.storage else {
2621                    return Ok(None);
2622                };
2623                ns.push(n);
2624                parts.push((s.as_ref(), &t.repacked_qs));
2625            }
2626            let storage = lhs.storage();
2627            let crate::Storage::Cpu(cpu) = &*storage else {
2628                return Ok(None);
2629            };
2630            let slice = cpu.as_slice::<f32>()?;
2631            let offset = lhs.layout().start_offset();
2632            let slice = &slice[offset..offset + k];
2633            let mut dsts: Vec<Vec<f32>> = ns.iter().map(|&n| vec![0f32; n]).collect();
2634            if !repack::try_gemv_fused(k, slice, &parts, &mut dsts)? {
2635                return Ok(None);
2636            }
2637            drop(storage);
2638            let outs = dsts
2639                .into_iter()
2640                .zip(&ns)
2641                .map(|(d, &n)| {
2642                    let mut shape = dims.to_vec();
2643                    *shape.last_mut().unwrap() = n;
2644                    Tensor::from_vec(d, shape, &crate::Device::Cpu)
2645                })
2646                .collect::<Result<Vec<_>>>()?;
2647            Ok(Some(outs))
2648        }
2649        #[cfg(target_arch = "x86_64")]
2650        {
2651            // the fused kernel is 512-bit only; downlevel tiers fall through to
2652            // per-matmul calls which dispatch to the 256-bit kernels
2653            if repack_x86::level() != Some(repack_x86::X86Level::Avx512Vnni) {
2654                return Ok(None);
2655            }
2656            if ts.is_empty() || !lhs.device().is_cpu() || lhs.dtype() != crate::DType::F32 {
2657                return Ok(None);
2658            }
2659            if !lhs.is_contiguous() {
2660                return Ok(None);
2661            }
2662            let dims = lhs.dims();
2663            let Some((&k, batch)) = dims.split_last() else {
2664                return Ok(None);
2665            };
2666            let m: usize = batch.iter().product::<usize>().max(1);
2667            let dtype = ts[0].dtype();
2668            let mut shapes = Vec::with_capacity(ts.len());
2669            for t in ts {
2670                let Ok((n, tk)) = t.shape.dims2() else {
2671                    return Ok(None);
2672                };
2673                if tk != k || t.dtype() != dtype || !repack_x86::select(dtype, n, k) {
2674                    return Ok(None);
2675                }
2676                shapes.push(n);
2677            }
2678            let guard = lhs.storage();
2679            let crate::Storage::Cpu(cpu) = &*guard else {
2680                return Ok(None);
2681            };
2682            let slice = cpu.as_slice::<f32>()?;
2683            let offset = lhs.layout().start_offset();
2684            let lhs_data = &slice[offset..offset + m * k];
2685            let lhs_q = repack_x86::quantize_lhs(lhs_data, m, k);
2686            let packs: Vec<&repack_x86::PackedX86> = ts
2687                .iter()
2688                .enumerate()
2689                .map(|(i, t)| {
2690                    let n = shapes[i];
2691                    t.repacked_qs
2692                        .x86_get_or_init(|| repack_x86::pack(dtype, t.storage_ref(), n, k))
2693                })
2694                .collect();
2695            let parts: Vec<(&repack_x86::PackedX86, usize)> = packs
2696                .iter()
2697                .zip(shapes.iter())
2698                .map(|(p, &n)| (*p, n))
2699                .collect();
2700            let mut dsts: Vec<Vec<f32>> = shapes.iter().map(|&n| vec![0f32; m * n]).collect();
2701            repack_x86::gemv_fused(&parts, &lhs_q, m, k, &mut dsts);
2702            drop(guard);
2703            let mut out = Vec::with_capacity(ts.len());
2704            let mut out_dims = dims.to_vec();
2705            for (dst, &n) in dsts.into_iter().zip(shapes.iter()) {
2706                *out_dims.last_mut().unwrap() = n;
2707                out.push(Tensor::from_vec(
2708                    dst,
2709                    out_dims.clone(),
2710                    &crate::Device::Cpu,
2711                )?);
2712            }
2713            Ok(Some(out))
2714        }
2715
2716        #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
2717        {
2718            let _ = (ts, lhs);
2719            Ok(None)
2720        }
2721    }
2722}
2723
2724impl QTensor {
2725    #[cfg(target_arch = "x86_64")]
2726    pub(crate) fn storage_ref(&self) -> &dyn QuantizedType {
2727        match &self.storage {
2728            QStorage::Cpu(s) => s.as_ref(),
2729            _ => unreachable!("cpu-only path"),
2730        }
2731    }
2732
2733    /// Indexed (MoE) matmul over stacked expert weights [n_experts, n_out, k]: each
2734    /// (token, expert-id) pair gemvs against its expert's rows via the repacked cache.
2735    /// Returns None when the layout, dtype, or device is unsupported.
2736    pub fn indexed_gemv(&self, x: &Tensor, ids: &Tensor) -> Result<Option<Tensor>> {
2737        #[cfg(target_arch = "aarch64")]
2738        {
2739            if !x.device().is_cpu()
2740                || !matches!(x.dtype(), crate::DType::F32 | crate::DType::BF16)
2741                || !x.is_contiguous()
2742            {
2743                return Ok(None);
2744            }
2745            let Ok((n_experts, n_out, k)) = self.shape.dims3() else {
2746                return Ok(None);
2747            };
2748            let Ok((batch, x_t, xk)) = x.dims3() else {
2749                return Ok(None);
2750            };
2751            let Ok((ids_b, topk)) = ids.dims2() else {
2752                return Ok(None);
2753            };
2754            if xk != k || ids_b != batch || (x_t != 1 && x_t != topk) {
2755                return Ok(None);
2756            }
2757            let QStorage::Cpu(storage) = &self.storage else {
2758                return Ok(None);
2759            };
2760            let ids_v: Vec<u32> = ids
2761                .to_dtype(crate::DType::U32)?
2762                .flatten_all()?
2763                .to_vec1::<u32>()?;
2764            if ids_v.iter().any(|&e| e as usize >= n_experts) {
2765                crate::bail!("expert index out of range");
2766            }
2767            let guard = x.storage();
2768            let crate::Storage::Cpu(cpu) = &*guard else {
2769                return Ok(None);
2770            };
2771            let offset = x.layout().start_offset();
2772            let n_rows = batch * x_t;
2773            let widened;
2774            let lhs: &[f32] = if x.dtype() == crate::DType::BF16 {
2775                let bslice = cpu.as_slice::<half::bf16>()?;
2776                widened = widen_bf16(&bslice[offset..offset + n_rows * k]);
2777                &widened
2778            } else {
2779                let slice = cpu.as_slice::<f32>()?;
2780                &slice[offset..offset + n_rows * k]
2781            };
2782            let mut dst = vec![0f32; batch * topk * n_out];
2783            let ok = repack::try_indexed_gemv(
2784                storage.as_ref(),
2785                &self.repacked_qs,
2786                n_experts,
2787                n_out,
2788                k,
2789                lhs,
2790                n_rows,
2791                &ids_v,
2792                topk,
2793                &mut dst,
2794            )?;
2795            drop(guard);
2796            if !ok {
2797                return Ok(None);
2798            }
2799            let out = Tensor::from_vec(dst, (batch, topk, n_out), &crate::Device::Cpu)?;
2800            if x.dtype() == crate::DType::BF16 {
2801                Ok(Some(out.to_dtype(crate::DType::BF16)?))
2802            } else {
2803                Ok(Some(out))
2804            }
2805        }
2806        #[cfg(not(target_arch = "aarch64"))]
2807        {
2808            let _ = (x, ids);
2809            Ok(None)
2810        }
2811    }
2812}
2813
2814fn widen_bf16(src: &[half::bf16]) -> Vec<f32> {
2815    let mut out: Vec<f32> = Vec::with_capacity(src.len());
2816    let out_ptr = out.as_mut_ptr() as usize;
2817    let n_units = src.len().div_ceil(WIDEN_CHUNK);
2818    crate::utils::barrier_pool().execute_chunked(n_units, |range| {
2819        let out_ptr = out_ptr as *mut f32;
2820        for unit in range {
2821            let lo = unit * WIDEN_CHUNK;
2822            let hi = src.len().min(lo + WIDEN_CHUNK);
2823            for (i, v) in src[lo..hi].iter().enumerate() {
2824                unsafe { *out_ptr.add(lo + i) = v.to_f32() };
2825            }
2826        }
2827    });
2828    // SAFETY: every element written by exactly one unit.
2829    unsafe { out.set_len(src.len()) };
2830    out
2831}
2832
2833fn narrow_bf16(src: &[f32]) -> Vec<half::bf16> {
2834    let mut out: Vec<half::bf16> = Vec::with_capacity(src.len());
2835    let out_ptr = out.as_mut_ptr() as usize;
2836    let n_units = src.len().div_ceil(WIDEN_CHUNK);
2837    crate::utils::barrier_pool().execute_chunked(n_units, |range| {
2838        let out_ptr = out_ptr as *mut half::bf16;
2839        for unit in range {
2840            let lo = unit * WIDEN_CHUNK;
2841            let hi = src.len().min(lo + WIDEN_CHUNK);
2842            for (i, v) in src[lo..hi].iter().enumerate() {
2843                unsafe { *out_ptr.add(lo + i) = half::bf16::from_f32(*v) };
2844            }
2845        }
2846    });
2847    // SAFETY: every element written by exactly one unit.
2848    unsafe { out.set_len(src.len()) };
2849    out
2850}
2851
2852const WIDEN_CHUNK: usize = 32 * 1024;
2853
2854impl crate::CustomOp1 for QTensor {
2855    fn name(&self) -> &'static str {
2856        "qmatmul"
2857    }
2858
2859    fn cpu_fwd(
2860        &self,
2861        storage: &crate::CpuStorage,
2862        layout: &crate::Layout,
2863    ) -> Result<(crate::CpuStorage, Shape)> {
2864        if !layout.is_contiguous() {
2865            crate::bail!("input tensor is not contiguous {layout:?}")
2866        }
2867        let src_shape = layout.shape();
2868        // self is transposed so n is first then k.
2869        let (n, k) = self.shape.dims2()?;
2870        if src_shape.rank() < 2 {
2871            crate::bail!("input tensor has only one dimension {layout:?}")
2872        }
2873        let mut dst_shape = src_shape.dims().to_vec();
2874        let last_k = dst_shape.pop().unwrap();
2875        if last_k != k {
2876            crate::bail!("input tensor {layout:?} incompatible with {:?}", self.shape)
2877        }
2878        dst_shape.push(n);
2879        let dst_shape = Shape::from(dst_shape);
2880        #[allow(clippy::infallible_destructuring_match)]
2881        let self_storage = match &self.storage {
2882            QStorage::Cpu(storage) => storage,
2883            #[cfg(feature = "rocm")]
2884            QStorage::Rocm(..) => crate::bail!("Invalid storage"),
2885            #[cfg(feature = "vulkan")]
2886            QStorage::Vulkan(..) => crate::bail!("Invalid storage"),
2887            #[cfg(feature = "wgpu")]
2888            QStorage::Wgpu(..) => crate::bail!("Invalid storage"),
2889            QStorage::Metal(_) | QStorage::Cuda(_) | QStorage::Stream(_) => {
2890                crate::bail!("Invalid storage")
2891            }
2892        };
2893        match storage.dtype() {
2894            DType::F32 => {
2895                let slice = storage.as_slice::<f32>()?;
2896                let slice =
2897                    &slice[layout.start_offset()..layout.start_offset() + src_shape.elem_count()];
2898                let mut dst_storage = vec![0f32; dst_shape.elem_count()];
2899
2900                let mkn = (dst_shape.elem_count() / n, k, n);
2901                let used_packed = repack::try_matmul_f32(
2902                    self_storage.as_ref(),
2903                    &self.repacked_qs,
2904                    mkn,
2905                    slice,
2906                    &mut dst_storage,
2907                )?;
2908                if used_packed {
2909                    return Ok((crate::CpuStorage::F32(dst_storage), dst_shape));
2910                }
2911
2912                self_storage.matmul_t(mkn, slice, &mut dst_storage)?;
2913                Ok((crate::CpuStorage::F32(dst_storage), dst_shape))
2914            }
2915            DType::F16 => {
2916                let slice = storage.as_slice::<f16>()?;
2917                let slice =
2918                    &slice[layout.start_offset()..layout.start_offset() + src_shape.elem_count()];
2919                let mut dst_storage = vec![f16::ZERO; dst_shape.elem_count()];
2920                self_storage.matmul_t_f16(
2921                    (dst_shape.elem_count() / n, k, n),
2922                    slice,
2923                    &mut dst_storage,
2924                )?;
2925                Ok((crate::CpuStorage::F16(dst_storage), dst_shape))
2926            }
2927            DType::BF16 => {
2928                // widen to f32 once and take the repacked path; output stays bf16
2929                let slice = storage.as_slice::<half::bf16>()?;
2930                let slice =
2931                    &slice[layout.start_offset()..layout.start_offset() + src_shape.elem_count()];
2932                let lhs = widen_bf16(slice);
2933                let mut dst_storage = vec![0f32; dst_shape.elem_count()];
2934
2935                let mkn = (dst_shape.elem_count() / n, k, n);
2936                let used_packed = repack::try_matmul_f32(
2937                    self_storage.as_ref(),
2938                    &self.repacked_qs,
2939                    mkn,
2940                    &lhs,
2941                    &mut dst_storage,
2942                )?;
2943                if !used_packed {
2944                    self_storage.matmul_t(mkn, &lhs, &mut dst_storage)?;
2945                }
2946                let dst: Vec<half::bf16> = narrow_bf16(&dst_storage);
2947                Ok((crate::CpuStorage::BF16(dst), dst_shape))
2948            }
2949            _ => crate::bail!("Expected f32/f16/bf16"),
2950        }
2951    }
2952
2953    fn metal_fwd(
2954        &self,
2955        storage: &crate::MetalStorage,
2956        layout: &crate::Layout,
2957    ) -> Result<(crate::MetalStorage, Shape)> {
2958        let self_storage = match &self.storage {
2959            QStorage::Metal(metal) => metal,
2960            _ => unreachable!("Cannot call metal matmul on non metal QTensor"),
2961        };
2962        self_storage.fwd(&self.shape, storage, layout)
2963    }
2964
2965    fn cuda_fwd(
2966        &self,
2967        storage: &crate::CudaStorage,
2968        layout: &crate::Layout,
2969    ) -> Result<(crate::CudaStorage, Shape)> {
2970        let self_storage = match &self.storage {
2971            QStorage::Cuda(cuda) => cuda,
2972            _ => unreachable!("Cannot call cuda matmul on non cuda QTensor"),
2973        };
2974        self_storage.fwd(&self.shape, storage, layout)
2975    }
2976}
2977
2978/// Dense (non-quantized) matmul `xs @ w^T` for the `Tensor`/`TensorF16` `QMatMul` variants, where
2979/// the stored weight `w` is `[n, k]` and `xs` is `[.., k]`. On ROCm at decode (a single-row matvec)
2980/// this computes the result as `sum_k(xs[k] * w[n, k])` via pooled broadcast-mul + reduce instead of
2981/// rocBLAS `gemm_ex`. rocBLAS's GEMM dispatch records a vendor-specific PM4 indirect-buffer packet
2982/// that WSL's HSA thunk rejects on hipGraph replay (`VendorSpecificAqlToPm4` assert), so a captured
2983/// decode forward containing one (e.g. the MoE F32 router gate) corrupts/aborts on replay. The
2984/// reduce path uses only ops already exercised under capture (RMSNorm etc.), so it replays cleanly,
2985/// and at M=1 a GEMV-as-reduce is as cheap as the GEMM (it materializes only the `[n, k]` weight).
2986/// Prefill (rows > 1, never graph-captured) keeps the rocBLAS GEMM. Non-ROCm devices are unchanged.
2987fn dense_matmul(xs: &Tensor, w: &Tensor) -> Result<Tensor> {
2988    let k = *w.dims().last().unwrap();
2989    let rows = xs.elem_count() / k;
2990    if rows == 1 && xs.device().is_rocm() {
2991        let n = w.dim(0)?;
2992        #[cfg(feature = "rocm")]
2993        {
2994            // Dense decode GEMV: read the [n,k] weight ONCE (warp/row dot) instead of materializing
2995            // and re-reading the broadcast_mul product. The activation is matched to the weight dtype
2996            // (a [k] cast, negligible); the GEMV stays capture-clean (no rocBLAS).
2997            let d = match xs.device() {
2998                Device::Rocm(d) => d.clone(),
2999                _ => unreachable!(),
3000            };
3001            let xs1 = xs.reshape((k,))?.to_dtype(w.dtype())?.contiguous()?;
3002            let w = w.contiguous()?;
3003            let (wstore, _) = w.storage_and_layout();
3004            let wr = match &*wstore {
3005                crate::Storage::Rocm(r) => r,
3006                _ => crate::bail!("dense_matmul: weight not on rocm"),
3007            };
3008            let (xstore, _) = xs1.storage_and_layout();
3009            let xr = match &*xstore {
3010                crate::Storage::Rocm(r) => r,
3011                _ => crate::bail!("dense_matmul: x not on rocm"),
3012            };
3013            let y = d.dense_gemv(wr, xr, n, k)?;
3014            let mut dims = xs.dims().to_vec();
3015            *dims.last_mut().unwrap() = n;
3016            return crate::tensor::from_storage(
3017                crate::Storage::Rocm(y),
3018                dims,
3019                crate::op::BackpropOp::none(),
3020                false,
3021            )
3022            .to_dtype(xs.dtype());
3023        }
3024        #[cfg(not(feature = "rocm"))]
3025        {
3026            let out = xs.reshape((1, k))?.broadcast_mul(w)?.sum(D::Minus1)?;
3027            let mut dims = xs.dims().to_vec();
3028            *dims.last_mut().unwrap() = n;
3029            return out.reshape(dims);
3030        }
3031    }
3032    let w = match *xs.dims() {
3033        [b1, b2, _, _] => w.broadcast_left((b1, b2))?.t()?,
3034        [bsize, _, _] => w.broadcast_left(bsize)?.t()?,
3035        _ => w.t()?,
3036    };
3037    xs.matmul(&w)
3038}
3039
3040// Prefill gate for the native Vulkan quantized GEMM. The `else` (dequant) path materializes a FULL f32
3041// copy of the weight (`QTensor::dequantize` -> `upload_f32` -> a fresh `amdgpu_bo_alloc`), ~100-235 MB
3042// per Linear weight. Under the deferred single command batch (`BATCH_CAP`) none of those f32 BOs free
3043// until the end-of-forward flush, so a dense prefill re-expands the whole model to f32 (~32 GB for an
3044// 8B) in fresh allocations; on the gfx1151 Strix Halo UMA the accumulated BOs exhaust VRAM+GTT and
3045// `GEM_CREATE` BLOCKS IN THE KERNEL waiting for a free that can only happen after the still-recording
3046// batch flushes -- a self-deadlock (engine hangs at the first long prefill). ROCm never hits it: its
3047// prefill rides the int8-WMMA `qmmq` GEMM, never dequantizes. So for every dtype that HAS a native
3048// quantized GEMM, ALWAYS use it (`usize::MAX`) -- liveness beats the at-large-M throughput edge the
3049// dequant path used to win (the GEMM re-reads the weight ceil(M/8) times so it is slower at big M, but
3050// it cannot deadlock; the future perf lever is a shared-memory-tiled int8 GEMM that reads the weight
3051// once, mirroring ROCm `qmmq`). Types WITHOUT a GEMM kernel return 0 so rows>1 dequantize (their only
3052// option; a handful of such weights allocate little and do not accumulate enough to deadlock). rows==1
3053// (decode) never reaches this gate -- it uses the native matvec straight out of the block format.
3054#[cfg(feature = "vulkan")]
3055fn vulkan_prefill_gemm_max_rows(dtype: GgmlDType) -> usize {
3056    match dtype {
3057        GgmlDType::Q4_0 | GgmlDType::Q8_0 | GgmlDType::Q4K | GgmlDType::Q5K | GgmlDType::Q6K => {
3058            usize::MAX
3059        }
3060        _ => 0,
3061    }
3062}
3063
3064// The native quant matvec/matmul kernels read the activation from the storage buffer base; they take
3065// no per-tensor start_offset. A tensor narrowed out of a larger buffer (e.g. extract_logits' last-row
3066// slice: `x.narrow(1, seq_len-1, 1)`) is `is_contiguous()` yet carries a non-zero start_offset because
3067// its size-1 leading dims mask the stride mismatch -- so `contiguous()` returns it as a view and the
3068// kernel would read the WRONG rows (position 0 instead of the narrowed one). Materialize a genuine
3069// offset-0 buffer before the storage reaches the kernel.
3070#[cfg(feature = "vulkan")]
3071fn vulkan_act_offset0(xs: &Tensor) -> Result<Tensor> {
3072    let xs = xs.contiguous()?;
3073    if xs.layout().start_offset() == 0 {
3074        Ok(xs)
3075    } else {
3076        xs.force_contiguous()
3077    }
3078}
3079
3080impl crate::Module for QMatMul {
3081    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
3082        match self {
3083            #[cfg(feature = "rocm")]
3084            Self::RocmQuant {
3085                qtensor,
3086                wq,
3087                dtype,
3088                n,
3089                k,
3090            } => {
3091                // Device-residency guard: multi-token attention prefill can leave the activation
3092                // off-device (an upstream op leaked to host); recover instead of bailing so prefill
3093                // stays correct. The leak is the bug to fix for speed; this keeps us correct meanwhile.
3094                let xs_recovered = if xs.device().is_rocm() {
3095                    None
3096                } else {
3097                    Some(xs.to_device(&qtensor.device())?)
3098                };
3099                let xs = xs_recovered.as_ref().unwrap_or(xs);
3100                let rows: usize = xs.elem_count() / *k;
3101                // Table-driven unified decode: a type is decode-native iff the single
3102                // `qmatvec_core<WTYPE>` has a `decode_block` wired for it (RocmQuantType::from_ggml).
3103                // Q8_0/Q4_0/Q4_K/Q6_K/IQ4_XS/TQ2_0 today; adding a type is one enum row, no kernel.
3104                // The dp4a-vs-scalar routing for dp4a-capable types lives entirely inside `matvec_quant`
3105                // (dp4a_active) -- ONE path, one place; the type stays native either way.
3106                #[cfg(feature = "rocm")]
3107                let unified_qt = crate::RocmQuantType::from_ggml(*dtype);
3108                #[cfg(not(feature = "rocm"))]
3109                let unified_qt: Option<()> = None;
3110                // Native int8-WMMA prefill exists only for `qmmq_capable` types; the decode-only types
3111                // (Q2_K/Q3_K + every IQ*/TQ* codebook/fractional type) dequantize-to-f16 for rows>1 via
3112                // the `else` branch below -- correct, just not WMMA-accelerated. ONE predicate, read here
3113                // and at the two MoE `use_qmmq` sites.
3114                #[cfg(feature = "rocm")]
3115                let qmmq_ok = unified_qt.map(|qt| qt.qmmq_capable()).unwrap_or(false);
3116                #[cfg(not(feature = "rocm"))]
3117                let qmmq_ok = false;
3118                if rows == 1 && unified_qt.is_some() {
3119                    // Decode: weights stay quantized in VRAM; the ONE native on-GPU quant matvec core
3120                    // dequantizes per-block on-the-fly (no dense f16 copy). The matvec consumes
3121                    // bf16/f16 activations directly and returns the same dtype, so the model's working
3122                    // dtype (bf16) is kept end-to-end -- no bf16->f32->f16->bf16 cast detour. Only fall
3123                    // back to an f16 cast for exotic input dtypes. Every wired type (symmetric 8-bit
3124                    // through asymmetric super-block through sub-4-bit ternary) rides the same core.
3125                    // dp4a-capable types accept the F32 residual/norm stream DIRECTLY (q8_1 quantize
3126                    // from f32 + f32-store matvec), so an F32 activation stays F32 end-to-end with no
3127                    // f16 bounce -- this removes the cast_f32_f16-before / cast_f16_f32-after pair that
3128                    // wrapped every decode matvec. Non-dp4a (scalar) types keep the f16 cast.
3129                    #[cfg(feature = "rocm")]
3130                    let keep_f32 = unified_qt.map(|qt| qt.dp4a_active()).unwrap_or(false);
3131                    #[cfg(not(feature = "rocm"))]
3132                    let keep_f32 = false;
3133                    let xs = match xs.dtype() {
3134                        DType::BF16 | DType::F16 => xs.contiguous()?,
3135                        DType::F32 if keep_f32 => xs.contiguous()?,
3136                        _ => xs.to_dtype(DType::F16)?.contiguous()?,
3137                    };
3138                    let d = match xs.device() {
3139                        Device::Rocm(d) => d,
3140                        _ => crate::bail!("RocmQuant input not on rocm"),
3141                    };
3142                    let y = {
3143                        let (store, _) = xs.storage_and_layout();
3144                        let xr = match &*store {
3145                            crate::Storage::Rocm(r) => r,
3146                            _ => crate::bail!("RocmQuant expected rocm storage"),
3147                        };
3148                        #[cfg(feature = "rocm")]
3149                        {
3150                            d.matvec_quant(unified_qt.unwrap(), wq, xr, *n, *k)?
3151                        }
3152                        #[cfg(not(feature = "rocm"))]
3153                        {
3154                            crate::bail!("rocm feature disabled")
3155                        }
3156                    };
3157                    let mut dims = xs.dims().to_vec();
3158                    let last = dims.len() - 1;
3159                    dims[last] = *n;
3160                    Ok(crate::tensor::from_storage(
3161                        crate::Storage::Rocm(y),
3162                        dims,
3163                        crate::op::BackpropOp::none(),
3164                        false,
3165                    ))
3166                } else if let Some(qt) = unified_qt.filter(|_| qmmq_ok) {
3167                    // Prefill (rows>1): native int8 WMMA gemm through the ONE unified core
3168                    // (`qmmq_core<WTYPE>` in quant.hip). Weights stay quantized in VRAM (no resident
3169                    // dense f16, which would slow the memory-bound decode) and the MAC runs on the
3170                    // RDNA3 int8 matrix cores instead of rocBLAS. The SAME core covers the whole wired
3171                    // spread: Q8_0/Q4_0 (symmetric, proven), Q4_K (asymmetric -- min bias via the
3172                    // q8_1 block-sum), and the symmetric super-block / IQ / ternary types (Q6_K,
3173                    // IQ4_XS, TQ2_0). Selecting the type is one `RocmQuantType` row + the in-kernel
3174                    // decode; there is NO per-quant prefill kernel.
3175                    let xs = xs.to_dtype(DType::F16)?.contiguous()?;
3176                    let d = match xs.device() {
3177                        Device::Rocm(d) => d,
3178                        _ => crate::bail!("RocmQuant input not on rocm"),
3179                    };
3180                    let m = xs.elem_count() / *k;
3181                    let y = {
3182                        let (store, _) = xs.storage_and_layout();
3183                        let xr = match &*store {
3184                            crate::Storage::Rocm(r) => r,
3185                            _ => crate::bail!("RocmQuant expected rocm storage"),
3186                        };
3187                        #[cfg(feature = "rocm")]
3188                        {
3189                            d.qmmq_quant(qt, xr, wq, m, *n, *k)?
3190                        }
3191                        #[cfg(not(feature = "rocm"))]
3192                        {
3193                            let _ = qt;
3194                            crate::bail!("rocm feature disabled")
3195                        }
3196                    };
3197                    let mut dims = xs.dims().to_vec();
3198                    let last = dims.len() - 1;
3199                    dims[last] = *n;
3200                    Ok(crate::tensor::from_storage(
3201                        crate::Storage::Rocm(y),
3202                        dims,
3203                        crate::op::BackpropOp::none(),
3204                        false,
3205                    ))
3206                } else {
3207                    // A type with no `qmmq_core<WTYPE>` wired (e.g. MXFP4): dequantize
3208                    // to a temporary f16 weight and multiply by that. It is freed after, since a
3209                    // persistent f16 copy would slow the memory-bound decode. The blocks are
3210                    // already in VRAM and decode there; the host copy is not read.
3211                    let w = match (unified_qt, xs.device()) {
3212                        #[cfg(feature = "rocm")]
3213                        (Some(qt), Device::Rocm(d)) => {
3214                            let dense = d.dequantize_quant(qt, wq, *n * *k, DType::F16)?;
3215                            crate::tensor::from_storage(
3216                                crate::Storage::Rocm(dense),
3217                                (*n, *k),
3218                                crate::op::BackpropOp::none(),
3219                                false,
3220                            )
3221                        }
3222                        _ => qtensor.dequantize_f16(&xs.device())?,
3223                    };
3224                    let w = match *xs.dims() {
3225                        [b1, b2, _, _] => w.broadcast_left((b1, b2))?.t()?,
3226                        [bsize, _, _] => w.broadcast_left(bsize)?.t()?,
3227                        _ => w.t()?,
3228                    };
3229                    xs.to_dtype(DType::F16)?.matmul(&w)
3230                }
3231            }
3232            #[cfg(feature = "vulkan")]
3233            Self::VulkanQuant {
3234                qtensor,
3235                wq,
3236                dtype,
3237                n,
3238                k,
3239            } => {
3240                // The activation can reach us on a different device than the weight -- e.g. a
3241                // CPU-resident prompt activation on the prefill path -- so place it on the weight's
3242                // Vulkan device rather than refusing. A no-op when it is already there, so the hot
3243                // decode path is unchanged; every sub-path below then sees a Vulkan input.
3244                let vdev = qtensor.device();
3245                let xs_on_vdev = if xs.device().same_device(&vdev) {
3246                    None
3247                } else {
3248                    Some(xs.to_device(&vdev)?)
3249                };
3250                let xs = xs_on_vdev.as_ref().unwrap_or(xs);
3251                let rows: usize = xs.elem_count() / *k;
3252                if rows == 1 {
3253                    // Decode: weights stay quantized in VRAM; the matching native-GGML quant matvec
3254                    // runs straight out of the block format (no dequant, no copy).
3255                    let xs = vulkan_act_offset0(xs)?;
3256                    let d = match xs.device() {
3257                        Device::Vulkan(d) => d,
3258                        _ => crate::bail!("VulkanQuant input not on vulkan"),
3259                    };
3260                    let y = {
3261                        let (store, _) = xs.storage_and_layout();
3262                        let xv = match &*store {
3263                            crate::Storage::Vulkan(v) => v,
3264                            _ => crate::bail!("VulkanQuant expected vulkan storage"),
3265                        };
3266                        match dtype {
3267                            GgmlDType::Q4_0 => d.matvec_q4_0_gpu(wq, xv, *n, *k)?,
3268                            // Q8_0 rides the 9-u32 repacked layout (mul_mat_vec_q8), the SAME blocks
3269                            // the prefill GEMM (mul_mat_q8) reads -- one layout for decode + prefill.
3270                            GgmlDType::Q8_0 => d.matvec_q8_gpu(wq, xv, *n, *k)?,
3271                            GgmlDType::Q4K => d.matvec_q4k_gpu(wq, xv, *n, *k)?,
3272                            GgmlDType::Q5K => d.matvec_q5k_gpu(wq, xv, *n, *k)?,
3273                            GgmlDType::Q6K => d.matvec_q6k_gpu(wq, xv, *n, *k)?,
3274                            GgmlDType::Q2K => d.matvec_q2k_gpu(wq, xv, *n, *k)?,
3275                            GgmlDType::Q3K => d.matvec_q3k_gpu(wq, xv, *n, *k)?,
3276                            GgmlDType::IQ4_XS => d.matvec_iq4xs_gpu(wq, xv, *n, *k)?,
3277                            GgmlDType::IQ4_NL => d.matvec_iq4nl_gpu(wq, xv, *n, *k)?,
3278                            GgmlDType::IQ2_XXS => d.matvec_iq2xxs_gpu(wq, xv, *n, *k)?,
3279                            GgmlDType::IQ2_XS => d.matvec_iq2xs_gpu(wq, xv, *n, *k)?,
3280                            GgmlDType::IQ1_M => d.matvec_iq1m_gpu(wq, xv, *n, *k)?,
3281                            GgmlDType::IQ1_S => d.matvec_iq1s_gpu(wq, xv, *n, *k)?,
3282                            GgmlDType::IQ3_S => d.matvec_iq3s_gpu(wq, xv, *n, *k)?,
3283                            GgmlDType::IQ3_XXS => d.matvec_iq3xxs_gpu(wq, xv, *n, *k)?,
3284                            GgmlDType::IQ2_S => d.matvec_iq2s_gpu(wq, xv, *n, *k)?,
3285                            GgmlDType::TQ2_0 => d.matvec_tq2_0_gpu(wq, xv, *n, *k)?,
3286                            other => crate::bail!("VulkanQuant: no native matvec for {other:?}"),
3287                        }
3288                    };
3289                    let mut dims = xs.dims().to_vec();
3290                    let last = dims.len() - 1;
3291                    dims[last] = *n;
3292                    Ok(crate::tensor::from_storage(
3293                        crate::Storage::Vulkan(y),
3294                        dims,
3295                        crate::op::BackpropOp::none(),
3296                        false,
3297                    ))
3298                } else if rows <= vulkan_prefill_gemm_max_rows(*dtype) {
3299                    // Prefill (small/moderate M): native quantized GEMM. Weights stay quantized in
3300                    // VRAM and each weight block is decoded ONCE per output column then reused across
3301                    // a tile of up to MATMUL_Q_MAX_M(=8) rows -- so the weight is re-read+re-decoded
3302                    // ceil(M/8) times, vs the dequant path's one-time f32 materialization. The GEMM
3303                    // therefore wins decisively while M is small (short / chunked prefill, batched
3304                    // decode) and would lose at large M, which the dtype-aware `rows` gate routes to
3305                    // the dequant path below. Same block layout + decode as the decode matvec above;
3306                    // one matmul_q*_gpu per native dtype. Leading batch dims flatten into M.
3307                    let m = rows;
3308                    let xs = vulkan_act_offset0(xs)?;
3309                    let d = match xs.device() {
3310                        Device::Vulkan(d) => d,
3311                        _ => crate::bail!("VulkanQuant input not on vulkan"),
3312                    };
3313                    let y = {
3314                        let (store, _) = xs.storage_and_layout();
3315                        let xv = match &*store {
3316                            crate::Storage::Vulkan(v) => v,
3317                            _ => crate::bail!("VulkanQuant expected vulkan storage"),
3318                        };
3319                        match dtype {
3320                            GgmlDType::Q4_0 => d.matmul_q4_0_gpu(wq, xv, m, *n, *k)?,
3321                            GgmlDType::Q8_0 => d.matmul_q8_gpu(wq, xv, m, *n, *k)?,
3322                            // Coopmat (tensor-core) Q4_K prefill GEMM. The mmq_q4k .spv bakes n/k, so
3323                            // only the committed shape routes here; the split bank is the 2D weight as
3324                            // a 1-expert MoE bank (cached), the activation q8_1-quantized once. Opt-in
3325                            // (VK_MMQ_Q4K) until measured against matmul_q4k_gpu with VK_PROFILE_GPU.
3326                            GgmlDType::Q4K
3327                                if *n == 2048
3328                                    && *k == 2048
3329                                    && std::env::var_os("VK_MMQ_Q4K").is_some() =>
3330                            {
3331                                let bank = qtensor.vulkan_moe_bank_split(d, 1, *n, *k)?;
3332                                let (xq, xsq, xsum) = d.quantize_act_q8(xv, m, *k)?;
3333                                d.mmq_q4k_gpu(&xq, &xsq, &xsum, bank.as_ref(), m, *n)?
3334                            }
3335                            GgmlDType::Q4K => d.matmul_q4k_gpu(wq, xv, m, *n, *k)?,
3336                            GgmlDType::Q5K => d.matmul_q5k_gpu(wq, xv, m, *n, *k)?,
3337                            GgmlDType::Q6K => d.matmul_q6k_gpu(wq, xv, m, *n, *k)?,
3338                            other => crate::bail!("VulkanQuant: no native matmul for {other:?}"),
3339                        }
3340                    };
3341                    let mut dims = xs.dims().to_vec();
3342                    let last = dims.len() - 1;
3343                    dims[last] = *n;
3344                    Ok(crate::tensor::from_storage(
3345                        crate::Storage::Vulkan(y),
3346                        dims,
3347                        crate::op::BackpropOp::none(),
3348                        false,
3349                    ))
3350                } else {
3351                    // Large dense prefill (M > the crossover): the column-per-invocation GEMM would
3352                    // re-read the weight ceil(M/8) times and lose to materializing the f32 weight once
3353                    // and running a dense matmul. Keep the dequant path here -- a strict non-regression
3354                    // until a shared-memory-tiled int8 GEMM (reads the weight once) removes the gate.
3355                    let w = qtensor.dequantize(&xs.device())?;
3356                    let w = match *xs.dims() {
3357                        [b1, b2, _, _] => w.broadcast_left((b1, b2))?.t()?,
3358                        [bsize, _, _] => w.broadcast_left(bsize)?.t()?,
3359                        _ => w.t()?,
3360                    };
3361                    xs.matmul(&w)
3362                }
3363            }
3364            #[cfg(feature = "wgpu")]
3365            Self::WgpuQuant {
3366                qtensor,
3367                wq,
3368                dtype,
3369                n,
3370                k,
3371            } => {
3372                // Same device coercion as the Vulkan arm: a CPU-resident prompt activation on the
3373                // prefill path is placed on the weight's Wgpu device rather than refused. A no-op
3374                // when it is already there, so the hot decode path is unchanged.
3375                let vdev = qtensor.device();
3376                let xs_on_vdev = if xs.device().same_device(&vdev) {
3377                    None
3378                } else {
3379                    Some(xs.to_device(&vdev)?)
3380                };
3381                let xs = xs_on_vdev.as_ref().unwrap_or(xs);
3382                let rows: usize = xs.elem_count() / *k;
3383                if rows == 1 {
3384                    // Decode: weights stay quantized in VRAM; the matching native-GGML quant matvec
3385                    // WGSL kernel runs straight out of the block format (no dequant, no copy).
3386                    let xs = xs.contiguous()?;
3387                    let d = match xs.device() {
3388                        Device::Wgpu(d) => d,
3389                        _ => crate::bail!("WgpuQuant input not on wgpu"),
3390                    };
3391                    let y = {
3392                        let (store, _) = xs.storage_and_layout();
3393                        let xv = match &*store {
3394                            crate::Storage::Wgpu(v) => v,
3395                            _ => crate::bail!("WgpuQuant expected wgpu storage"),
3396                        };
3397                        match dtype {
3398                            GgmlDType::Q4_0 => d.matvec_q4_0_gpu(wq, xv, *n, *k)?,
3399                            GgmlDType::Q8_0 => d.matvec_q8_0_gpu(wq, xv, *n, *k)?,
3400                            GgmlDType::Q4K => d.matvec_q4k_gpu(wq, xv, *n, *k)?,
3401                            other => crate::bail!("WgpuQuant: no native matvec for {other:?}"),
3402                        }
3403                    };
3404                    let mut dims = xs.dims().to_vec();
3405                    let last = dims.len() - 1;
3406                    dims[last] = *n;
3407                    Ok(crate::tensor::from_storage(
3408                        crate::Storage::Wgpu(y),
3409                        dims,
3410                        crate::op::BackpropOp::none(),
3411                        false,
3412                    ))
3413                } else {
3414                    // Prefill: dequantize to a temporary f32 weight (reuses the NT matmul path).
3415                    let w = qtensor.dequantize(&xs.device())?;
3416                    let w = match *xs.dims() {
3417                        [b1, b2, _, _] => w.broadcast_left((b1, b2))?.t()?,
3418                        [bsize, _, _] => w.broadcast_left(bsize)?.t()?,
3419                        _ => w.t()?,
3420                    };
3421                    xs.matmul(&w)
3422                }
3423            }
3424            Self::QTensor(t) => xs.apply_op1_no_bwd(t.as_ref()),
3425            Self::Tensor(w) => dense_matmul(xs, w),
3426            Self::TensorF16(w) => {
3427                let in_dtype = xs.dtype();
3428                dense_matmul(&xs.to_dtype(DType::F16)?, w)?.to_dtype(in_dtype)
3429            }
3430        }
3431    }
3432}
3433
3434#[cfg(test)]
3435mod tests {
3436    use super::GgmlDType;
3437
3438    #[test]
3439    fn block_align_divides_type_size() {
3440        // `from_u32` and `block_align` are generated from the same `for_each_quant!` table, so
3441        // walking the id space visits every dtype.
3442        let dtypes: Vec<GgmlDType> = (0..1024u32)
3443            .filter_map(|id| GgmlDType::from_u32(id).ok())
3444            .collect();
3445        for dtype in [
3446            GgmlDType::F32,
3447            GgmlDType::BF16,
3448            GgmlDType::I32,
3449            GgmlDType::NVFP4,
3450        ] {
3451            assert!(dtypes.contains(&dtype), "{dtype:?} has no GGML id");
3452        }
3453        for dtype in dtypes {
3454            assert_eq!(
3455                GgmlDType::from_u32(dtype.to_u32()).unwrap(),
3456                dtype,
3457                "{dtype:?} does not survive a to_u32/from_u32 round trip"
3458            );
3459            let (size, align) = (dtype.type_size(), dtype.block_align());
3460            assert!(align.is_power_of_two(), "{dtype:?}: align {align}");
3461            assert_eq!(
3462                size % align,
3463                0,
3464                "{dtype:?}: type_size {size} block_align {align}"
3465            );
3466        }
3467    }
3468}