Skip to main content

hanzo_ml/quantized/
mod.rs

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