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