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