1use std::collections::HashMap;
38
39use safetensors::Dtype;
40use safetensors::tensor::TensorView;
41use serde::{Deserialize, Serialize};
42
43use crate::error::Result;
44use crate::quant::pack4::pack_i4;
45use crate::quant::{Fp8Variant, Granularity, Scheme, dequantize_affine, dequantize_fp8};
46use crate::quant::{quantize_affine, quantize_fp8};
47use crate::read::is_quantizable_float;
48use crate::write::OutputTensor;
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct WeightsConfig {
53 pub num_bits: u8,
55 #[serde(rename = "type")]
57 pub type_: String,
58 pub symmetric: bool,
60 pub strategy: String,
62 #[serde(skip_serializing_if = "Option::is_none")]
66 pub axis: Option<usize>,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub group_size: Option<usize>,
70 #[serde(skip_serializing_if = "Option::is_none")]
74 pub fp8_variant: Option<String>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ConfigGroup {
80 pub weights: WeightsConfig,
82 pub targets: Vec<String>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct PackedTensorInfo {
92 pub logical_shape: Vec<usize>,
94 pub elements: usize,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct QuantizationConfig {
101 pub quant_method: String,
103 pub format: String,
105 pub config_groups: HashMap<String, ConfigGroup>,
107 #[serde(default, skip_serializing_if = "Vec::is_empty")]
109 pub ignore: Vec<String>,
110 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
112 pub teenygrad_packed_int4: HashMap<String, PackedTensorInfo>,
113}
114
115pub const QUANTIZATION_CONFIG_KEY: &str = "quantization_config";
117
118pub fn scale_tensor_name(weight_name: &str) -> String {
120 format!("{weight_name}_scale")
121}
122
123pub fn zero_point_tensor_name(weight_name: &str) -> String {
125 format!("{weight_name}_zero_point")
126}
127
128pub fn should_quantize(shape: &[usize], dtype: Dtype) -> bool {
131 shape.len() >= 2 && is_quantizable_float(dtype)
132}
133
134fn f32_vec_to_le_bytes(values: &[f32]) -> Vec<u8> {
135 values.iter().flat_map(|v| v.to_le_bytes()).collect()
136}
137
138fn i32_vec_to_le_bytes(values: &[i32]) -> Vec<u8> {
139 values.iter().flat_map(|v| v.to_le_bytes()).collect()
140}
141
142pub fn quantize_tensor(
146 name: &str,
147 data: &[f32],
148 shape: &[usize],
149 scheme: Scheme,
150 granularity: Granularity,
151) -> Result<HashMap<String, OutputTensor>> {
152 let mut out = HashMap::with_capacity(3);
153
154 match scheme {
155 Scheme::Int8 { symmetric } => {
156 let q = quantize_affine(name, data, shape, granularity, symmetric, 8)?;
157 let qbytes: Vec<u8> = q.qvalues.iter().map(|&v| v as u8).collect();
158 out.insert(
159 name.to_string(),
160 OutputTensor {
161 dtype: Dtype::I8,
162 shape: shape.to_vec(),
163 data: qbytes,
164 },
165 );
166 let scales: Vec<f32> = q.params.iter().map(|p| p.scale).collect();
167 out.insert(
168 scale_tensor_name(name),
169 OutputTensor {
170 dtype: Dtype::F32,
171 shape: vec![scales.len()],
172 data: f32_vec_to_le_bytes(&scales),
173 },
174 );
175 if !symmetric {
176 let zps: Vec<i32> = q.params.iter().map(|p| p.zero_point).collect();
177 out.insert(
178 zero_point_tensor_name(name),
179 OutputTensor {
180 dtype: Dtype::I32,
181 shape: vec![zps.len()],
182 data: i32_vec_to_le_bytes(&zps),
183 },
184 );
185 }
186 }
187 Scheme::Int4 { symmetric } => {
188 let q = quantize_affine(name, data, shape, granularity, symmetric, 4)?;
189 let packed = pack_i4(&q.qvalues);
190 out.insert(
191 name.to_string(),
192 OutputTensor {
193 dtype: Dtype::U8,
194 shape: vec![packed.len()],
195 data: packed,
196 },
197 );
198 let scales: Vec<f32> = q.params.iter().map(|p| p.scale).collect();
199 out.insert(
200 scale_tensor_name(name),
201 OutputTensor {
202 dtype: Dtype::F32,
203 shape: vec![scales.len()],
204 data: f32_vec_to_le_bytes(&scales),
205 },
206 );
207 if !symmetric {
208 let zps: Vec<i32> = q.params.iter().map(|p| p.zero_point).collect();
209 out.insert(
210 zero_point_tensor_name(name),
211 OutputTensor {
212 dtype: Dtype::I32,
213 shape: vec![zps.len()],
214 data: i32_vec_to_le_bytes(&zps),
215 },
216 );
217 }
218 }
219 Scheme::Fp8 { variant } => {
220 let q = quantize_fp8(data, shape, granularity, variant);
221 let dtype = match variant {
222 Fp8Variant::E4M3 => Dtype::F8_E4M3,
223 Fp8Variant::E5M2 => Dtype::F8_E5M2,
224 };
225 out.insert(
226 name.to_string(),
227 OutputTensor {
228 dtype,
229 shape: shape.to_vec(),
230 data: q.qvalues.clone(),
231 },
232 );
233 out.insert(
234 scale_tensor_name(name),
235 OutputTensor {
236 dtype: Dtype::F32,
237 shape: vec![q.scales.len()],
238 data: f32_vec_to_le_bytes(&q.scales),
239 },
240 );
241 }
242 }
243
244 Ok(out)
245}
246
247pub fn dequantize_tensor(
250 scheme: Scheme,
251 qview: &TensorView<'_>,
252 shape: &[usize],
253 scales: &[f32],
254 zero_points: Option<&[i32]>,
255 groups: &[u32],
256) -> Vec<f32> {
257 match scheme {
258 Scheme::Int8 { .. } | Scheme::Int4 { .. } => {
259 let qvalues: Vec<i8> = match scheme {
260 Scheme::Int4 { .. } => {
261 let n: usize = shape.iter().product();
262 crate::quant::pack4::unpack_i4(qview.data(), n)
263 }
264 _ => qview.data().iter().map(|&b| b as i8).collect(),
265 };
266 let params: Vec<crate::quant::AffineParams> = scales
267 .iter()
268 .enumerate()
269 .map(|(i, &scale)| crate::quant::AffineParams {
270 scale,
271 zero_point: zero_points.map(|z| z[i]).unwrap_or(0),
272 })
273 .collect();
274 dequantize_affine(&crate::quant::QuantizedAffine {
275 qvalues,
276 params,
277 groups: groups.to_vec(),
278 })
279 }
280 Scheme::Fp8 { variant } => dequantize_fp8(&crate::quant::QuantizedFp8 {
281 qvalues: qview.data().to_vec(),
282 scales: scales.to_vec(),
283 groups: groups.to_vec(),
284 variant,
285 }),
286 }
287}
288
289pub fn build_config(
292 scheme: Scheme,
293 granularity: Granularity,
294 ignored: Vec<String>,
295 packed_int4: HashMap<String, PackedTensorInfo>,
296) -> QuantizationConfig {
297 let format = match scheme {
298 Scheme::Int8 { .. } => "int-quantized",
299 Scheme::Int4 { .. } => "pack-quantized",
300 Scheme::Fp8 { .. } => "float-quantized",
301 };
302
303 let group_size = match granularity {
304 Granularity::Group { group_size, .. } => Some(group_size),
305 _ => None,
306 };
307 let axis = granularity.axis_and_group_size().map(|(axis, _)| axis);
308 let fp8_variant = match scheme {
309 Scheme::Fp8 {
310 variant: Fp8Variant::E4M3,
311 } => Some("e4m3".to_string()),
312 Scheme::Fp8 {
313 variant: Fp8Variant::E5M2,
314 } => Some("e5m2".to_string()),
315 _ => None,
316 };
317
318 let weights = WeightsConfig {
319 num_bits: scheme.num_bits(),
320 type_: scheme.type_name().to_string(),
321 symmetric: scheme.is_symmetric(),
322 strategy: granularity.strategy_name().to_string(),
323 axis,
324 group_size,
325 fp8_variant,
326 };
327
328 let mut config_groups = HashMap::new();
329 config_groups.insert(
330 "group_0".to_string(),
331 ConfigGroup {
332 weights,
333 targets: vec!["*".to_string()],
334 },
335 );
336
337 QuantizationConfig {
338 quant_method: "compressed-tensors".to_string(),
339 format: format.to_string(),
340 config_groups,
341 ignore: ignored,
342 teenygrad_packed_int4: packed_int4,
343 }
344}
345
346pub fn config_to_metadata(config: &QuantizationConfig) -> Result<HashMap<String, String>> {
349 let mut metadata = HashMap::new();
350 metadata.insert(
351 QUANTIZATION_CONFIG_KEY.to_string(),
352 serde_json::to_string(config)?,
353 );
354 Ok(metadata)
355}
356
357pub fn config_from_metadata(
359 metadata: &HashMap<String, String>,
360) -> Result<Option<QuantizationConfig>> {
361 match metadata.get(QUANTIZATION_CONFIG_KEY) {
362 Some(json) => Ok(Some(serde_json::from_str(json)?)),
363 None => Ok(None),
364 }
365}