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