1use anyhow::{Context, Result, bail};
28use safetensors::SafeTensors;
29use serde_json::Value as JsonValue;
30
31use crate::gpu::{DevicePtr, GpuBackend, KernelArg};
32
33#[derive(Debug, Clone, Copy)]
38pub struct MlxQuantConfig {
39 pub bits: u32,
40 pub group_size: u32,
41}
42
43impl MlxQuantConfig {
44 pub fn from_config(config: &JsonValue) -> Option<Self> {
49 let q = config
50 .get("quantization")
51 .or_else(|| config.get("quantization_config"))?;
52 let bits = q.get("bits")?.as_u64()? as u32;
53 let group_size = q.get("group_size")?.as_u64()? as u32;
54 Some(Self { bits, group_size })
55 }
56}
57
58pub struct MlxInt8Weight {
64 pub packed: DevicePtr,
66 pub scales: DevicePtr,
68 pub biases: DevicePtr,
70 pub out_features: u32,
71 pub in_features: u32,
72 pub group_size: u32,
73}
74
75impl MlxInt8Weight {
76 pub fn load(
80 gpu: &dyn GpuBackend,
81 st: &SafeTensors,
82 base: &str,
83 group_size: u32,
84 ) -> Result<Self> {
85 let weight_name = format!("{base}.weight");
86 let scales_name = format!("{base}.scales");
87 let biases_name = format!("{base}.biases");
88
89 let weight = st
90 .tensor(&weight_name)
91 .with_context(|| format!("missing tensor {weight_name}"))?;
92 let scales = st
93 .tensor(&scales_name)
94 .with_context(|| format!("missing tensor {scales_name}"))?;
95 let biases = st
96 .tensor(&biases_name)
97 .with_context(|| format!("missing tensor {biases_name}"))?;
98
99 if weight.dtype() != safetensors::Dtype::U32 {
100 bail!(
101 "{weight_name}: expected U32 (MLX 8-bit packed), got {:?}",
102 weight.dtype()
103 );
104 }
105 if scales.dtype() != safetensors::Dtype::BF16 || biases.dtype() != safetensors::Dtype::BF16
106 {
107 bail!(
108 "{base}.scales/biases: expected BF16, got scales={:?}, biases={:?}",
109 scales.dtype(),
110 biases.dtype()
111 );
112 }
113
114 let weight_shape = weight.shape();
115 if weight_shape.len() != 2 {
116 bail!(
117 "{weight_name}: expected 2-D weight tensor, got rank {}",
118 weight_shape.len()
119 );
120 }
121 let out_features = weight_shape[0] as u32;
122 let packed_cols = weight_shape[1] as u32;
123 let in_features = packed_cols * 4;
124
125 let groups_per_row = in_features
126 .checked_div(group_size)
127 .filter(|&g| g * group_size == in_features)
128 .ok_or_else(|| {
129 anyhow::anyhow!(
130 "{base}: in_features {in_features} not divisible by group_size {group_size}"
131 )
132 })?;
133
134 let expected = [out_features as usize, groups_per_row as usize];
135 if scales.shape() != expected {
136 bail!(
137 "{}.scales: expected shape {:?}, got {:?}",
138 base,
139 expected,
140 scales.shape()
141 );
142 }
143 if biases.shape() != expected {
144 bail!(
145 "{}.biases: expected shape {:?}, got {:?}",
146 base,
147 expected,
148 biases.shape()
149 );
150 }
151
152 let packed_ptr = gpu.alloc(weight.data().len())?;
153 gpu.copy_h2d(weight.data(), packed_ptr)?;
154 let scales_ptr = gpu.alloc(scales.data().len())?;
155 gpu.copy_h2d(scales.data(), scales_ptr)?;
156 let biases_ptr = gpu.alloc(biases.data().len())?;
157 gpu.copy_h2d(biases.data(), biases_ptr)?;
158
159 Ok(Self {
160 packed: packed_ptr,
161 scales: scales_ptr,
162 biases: biases_ptr,
163 out_features,
164 in_features,
165 group_size,
166 })
167 }
168
169 pub fn dequantize_to(&self, gpu: &dyn GpuBackend, out: DevicePtr, stream: u64) -> Result<()> {
174 let kernel = gpu.kernel("mlx_int8_dequant", "mlx_int8_dequant")?;
175 let block_x: u32 = 16;
178 let block_y: u32 = 1;
179 let grid_x = self.in_features.div_ceil(block_x);
180 let grid_y = self.out_features;
181 gpu.launch_typed(
182 kernel,
183 [grid_x, grid_y, 1],
184 [block_x, block_y, 1],
185 0,
186 stream,
187 &[
188 KernelArg::Bytes(&self.out_features.to_le_bytes()),
189 KernelArg::Bytes(&self.in_features.to_le_bytes()),
190 KernelArg::Bytes(&self.group_size.to_le_bytes()),
191 KernelArg::Buffer(self.packed),
192 KernelArg::Buffer(self.scales),
193 KernelArg::Buffer(self.biases),
194 KernelArg::Buffer(out),
195 ],
196 )
197 }
198
199 pub fn gemv(
203 &self,
204 gpu: &dyn GpuBackend,
205 x: DevicePtr,
206 y: DevicePtr,
207 stream: u64,
208 ) -> Result<()> {
209 let kernel = gpu.kernel("mlx_int8_gemv", "mlx_int8_gemv")?;
210 const ROWS_PER_TG: u32 = 4;
215 const SIMDGROUP_SIZE: u32 = 32;
216 let threads_per_tg: u32 = ROWS_PER_TG * SIMDGROUP_SIZE; let row_groups = self.out_features.div_ceil(ROWS_PER_TG);
218 gpu.launch_typed(
219 kernel,
220 [row_groups, 1, 1],
221 [threads_per_tg, 1, 1],
222 0,
223 stream,
224 &[
225 KernelArg::Bytes(&self.out_features.to_le_bytes()),
226 KernelArg::Bytes(&self.in_features.to_le_bytes()),
227 KernelArg::Bytes(&self.group_size.to_le_bytes()),
228 KernelArg::Buffer(self.packed),
229 KernelArg::Buffer(self.scales),
230 KernelArg::Buffer(self.biases),
231 KernelArg::Buffer(x),
232 KernelArg::Buffer(y),
233 ],
234 )
235 }
236
237 pub fn gemv_silu_gate_resid(
243 &self,
244 gpu: &dyn GpuBackend,
245 gate: DevicePtr,
246 up: DevicePtr,
247 x_resid: DevicePtr,
248 y: DevicePtr,
249 stream: u64,
250 ) -> Result<()> {
251 let kernel = gpu.kernel("mlx_int8_gemv_silu_gate", "mlx_int8_gemv_silu_gate_resid")?;
252 const ROWS_PER_TG: u32 = 4;
253 const SIMDGROUP_SIZE: u32 = 32;
254 let threads_per_tg: u32 = ROWS_PER_TG * SIMDGROUP_SIZE;
255 let row_groups = self.out_features.div_ceil(ROWS_PER_TG);
256 gpu.launch_typed(
257 kernel,
258 [row_groups, 1, 1],
259 [threads_per_tg, 1, 1],
260 0,
261 stream,
262 &[
263 KernelArg::Bytes(&self.out_features.to_le_bytes()),
264 KernelArg::Bytes(&self.in_features.to_le_bytes()),
265 KernelArg::Bytes(&self.group_size.to_le_bytes()),
266 KernelArg::Buffer(self.packed),
267 KernelArg::Buffer(self.scales),
268 KernelArg::Buffer(self.biases),
269 KernelArg::Buffer(gate),
270 KernelArg::Buffer(up),
271 KernelArg::Buffer(x_resid),
272 KernelArg::Buffer(y),
273 ],
274 )
275 }
276
277 pub fn gemv_silu_gate(
283 &self,
284 gpu: &dyn GpuBackend,
285 gate: DevicePtr,
286 up: DevicePtr,
287 y: DevicePtr,
288 stream: u64,
289 ) -> Result<()> {
290 let kernel = gpu.kernel("mlx_int8_gemv_silu_gate", "mlx_int8_gemv_silu_gate")?;
291 const ROWS_PER_TG: u32 = 4;
292 const SIMDGROUP_SIZE: u32 = 32;
293 let threads_per_tg: u32 = ROWS_PER_TG * SIMDGROUP_SIZE;
294 let row_groups = self.out_features.div_ceil(ROWS_PER_TG);
295 gpu.launch_typed(
296 kernel,
297 [row_groups, 1, 1],
298 [threads_per_tg, 1, 1],
299 0,
300 stream,
301 &[
302 KernelArg::Bytes(&self.out_features.to_le_bytes()),
303 KernelArg::Bytes(&self.in_features.to_le_bytes()),
304 KernelArg::Bytes(&self.group_size.to_le_bytes()),
305 KernelArg::Buffer(self.packed),
306 KernelArg::Buffer(self.scales),
307 KernelArg::Buffer(self.biases),
308 KernelArg::Buffer(gate),
309 KernelArg::Buffer(up),
310 KernelArg::Buffer(y),
311 ],
312 )
313 }
314
315 pub fn gemm(
320 &self,
321 gpu: &dyn GpuBackend,
322 x: DevicePtr,
323 y: DevicePtr,
324 m: u32,
325 stream: u64,
326 ) -> Result<()> {
327 let kernel = gpu.kernel("mlx_int8_gemm", "mlx_int8_gemm")?;
328 let block_x: u32 = 16;
329 let block_y: u32 = 16;
330 let grid_x = self.out_features.div_ceil(block_x);
331 let grid_y = m.div_ceil(block_y);
332 gpu.launch_typed(
333 kernel,
334 [grid_x, grid_y, 1],
335 [block_x, block_y, 1],
336 0,
337 stream,
338 &[
339 KernelArg::Bytes(&m.to_le_bytes()),
340 KernelArg::Bytes(&self.out_features.to_le_bytes()),
341 KernelArg::Bytes(&self.in_features.to_le_bytes()),
342 KernelArg::Bytes(&self.group_size.to_le_bytes()),
343 KernelArg::Buffer(x),
344 KernelArg::Buffer(self.packed),
345 KernelArg::Buffer(self.scales),
346 KernelArg::Buffer(self.biases),
347 KernelArg::Buffer(y),
348 ],
349 )
350 }
351
352 pub fn release(&self, gpu: &dyn GpuBackend) -> Result<()> {
357 gpu.free(self.packed)?;
358 gpu.free(self.scales)?;
359 gpu.free(self.biases)?;
360 Ok(())
361 }
362}
363
364pub fn gemv_gate_up(
369 gpu: &dyn GpuBackend,
370 gate: &MlxInt8Weight,
371 up: &MlxInt8Weight,
372 x: DevicePtr,
373 gate_y: DevicePtr,
374 up_y: DevicePtr,
375 stream: u64,
376) -> Result<()> {
377 debug_assert_eq!(gate.out_features, up.out_features);
378 debug_assert_eq!(gate.in_features, up.in_features);
379 debug_assert_eq!(gate.group_size, up.group_size);
380 let kernel = gpu.kernel("mlx_int8_gemv_gate_up", "mlx_int8_gemv_gate_up")?;
381 const ROWS_PER_TG: u32 = 4;
382 const SIMDGROUP_SIZE: u32 = 32;
383 let threads_per_tg: u32 = ROWS_PER_TG * SIMDGROUP_SIZE;
384 let row_groups = gate.out_features.div_ceil(ROWS_PER_TG);
385 gpu.launch_typed(
386 kernel,
387 [row_groups, 1, 1],
388 [threads_per_tg, 1, 1],
389 0,
390 stream,
391 &[
392 KernelArg::Bytes(&gate.out_features.to_le_bytes()),
393 KernelArg::Bytes(&gate.in_features.to_le_bytes()),
394 KernelArg::Bytes(&gate.group_size.to_le_bytes()),
395 KernelArg::Buffer(gate.packed),
396 KernelArg::Buffer(gate.scales),
397 KernelArg::Buffer(gate.biases),
398 KernelArg::Buffer(up.packed),
399 KernelArg::Buffer(up.scales),
400 KernelArg::Buffer(up.biases),
401 KernelArg::Buffer(x),
402 KernelArg::Buffer(gate_y),
403 KernelArg::Buffer(up_y),
404 ],
405 )
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 #[test]
413 fn parse_quant_config_from_mlx_layout() {
414 let cfg: JsonValue = serde_json::json!({
415 "quantization": { "bits": 8, "group_size": 64, "mode": "affine" },
416 "model_type": "qwen3_5",
417 });
418 let q = MlxQuantConfig::from_config(&cfg).expect("expected quant block");
419 assert_eq!(q.bits, 8);
420 assert_eq!(q.group_size, 64);
421 }
422
423 #[test]
424 fn parse_quant_config_falls_back_to_quantization_config() {
425 let cfg: JsonValue = serde_json::json!({
426 "quantization_config": { "bits": 8, "group_size": 64 },
427 });
428 let q = MlxQuantConfig::from_config(&cfg).expect("expected quant_config block");
429 assert_eq!(q.bits, 8);
430 assert_eq!(q.group_size, 64);
431 }
432
433 #[test]
434 fn parse_quant_config_returns_none_when_absent() {
435 let cfg: JsonValue = serde_json::json!({ "model_type": "qwen3_5" });
436 assert!(MlxQuantConfig::from_config(&cfg).is_none());
437 }
438}