Skip to main content

zsfm_chronos/
convert.rs

1use std::fs::File;
2use std::io::BufWriter;
3use std::path::Path;
4
5use anyhow::Context;
6use indicatif::{ProgressBar, ProgressStyle};
7use safetensors::Dtype as StDtype;
8use safetensors::SafeTensors;
9
10use zsfm_gguf::{GGMLType, GGUFMetaValue, GGUFWriter};
11use zsfm_hub::ModelFiles;
12
13use crate::config::Chronos2Config;
14use crate::tensor_map::map_tensor_name;
15
16/// Options that control how tensors are stored in the output GGUF.
17pub struct ConvertOptions {
18    pub output_dtype: GGMLType,
19}
20
21/// Read `files`, convert every tensor, and write a GGUF file to `output_path`.
22pub fn convert(
23    model_id: &str,
24    files: &ModelFiles,
25    config: &Chronos2Config,
26    opts: &ConvertOptions,
27    output_path: &Path,
28) -> anyhow::Result<()> {
29    let mut writer = GGUFWriter::new();
30
31    write_metadata(&mut writer, model_id, config);
32
33    let shard_bytes = load_shard_bytes(&files.safetensors_shards)?;
34    let shard_views: Vec<SafeTensors> = shard_bytes
35        .iter()
36        .map(|b| SafeTensors::deserialize(b).context("deserialize shard"))
37        .collect::<anyhow::Result<_>>()?;
38
39    let total_tensors: usize = shard_views.iter().map(|s| s.len()).sum();
40    println!(
41        "Found {} tensors across {} shard(s).",
42        total_tensors,
43        shard_views.len()
44    );
45
46    let pb = ProgressBar::new(total_tensors as u64);
47    pb.set_style(
48        ProgressStyle::with_template(
49            "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}",
50        )
51        .unwrap()
52        .progress_chars("=>-"),
53    );
54
55    let mut mapped = 0usize;
56    let mut skipped: Vec<String> = Vec::new();
57    let mut fallback_count = 0usize;
58
59    for shard in &shard_views {
60        for (hf_name, tensor_view) in shard.tensors() {
61            pb.set_message(hf_name.to_string());
62
63            let gguf_name = match map_tensor_name(&hf_name) {
64                Some(n) => n,
65                None => {
66                    skipped.push(hf_name.to_string());
67                    pb.inc(1);
68                    continue;
69                }
70            };
71
72            let src_dtype = ggml_type_from_st(tensor_view.dtype()).with_context(|| {
73                format!(
74                    "tensor {hf_name}: unsupported dtype {:?}",
75                    tensor_view.dtype()
76                )
77            })?;
78
79            let raw_data = tensor_view.data();
80            let py_shape = tensor_view.shape();
81            let n_elems: usize = py_shape.iter().product();
82            let innermost = py_shape.last().copied().unwrap_or(1);
83            let outermost = py_shape.first().copied().unwrap_or(1);
84
85            // Q8_0: Q8_0 blocks need the innermost dim (last Python dim, first GGUF dim) to be
86            // divisible by 32. When it isn't for a 2D weight whose outermost dim is aligned,
87            // transpose data and store GGUF shape in Python order (not reversed). The GGUF shape as stored
88            // is reversed back at load time, placing the aligned dim innermost. The inference
89            // loader detects the transposition by checking dim(0) against the expected d_out.
90            let (dst_dtype, gguf_shape, tensor_data) = if opts.output_dtype == GGMLType::Q8_0
91                && (innermost % 32 != 0 || n_elems % 32 != 0)
92            {
93                fallback_count += 1;
94                let data = cast_data(raw_data, src_dtype, GGMLType::F32)
95                    .with_context(|| format!("tensor {hf_name}: cast failed"))?;
96                let gs = py_shape.iter().rev().map(|&d| d as u64).collect();
97                (GGMLType::F32, gs, data)
98            } else {
99                let dst = opts.output_dtype;
100                let data = cast_data(raw_data, src_dtype, dst)
101                    .with_context(|| format!("tensor {hf_name}: cast failed"))?;
102                let gs = py_shape.iter().rev().map(|&d| d as u64).collect();
103                (dst, gs, data)
104            };
105
106            writer.add_tensor(gguf_name, gguf_shape, dst_dtype, tensor_data);
107            mapped += 1;
108            pb.inc(1);
109        }
110    }
111
112    pb.finish_with_message("tensors processed");
113
114    if !skipped.is_empty() {
115        eprintln!(
116            "\nWarning: {} tensor(s) had unrecognised names and were skipped:",
117            skipped.len()
118        );
119        for name in &skipped {
120            eprintln!("  {name}");
121        }
122        eprintln!("Update tensor_map.rs to include these if needed.");
123    }
124
125    if fallback_count > 0 {
126        eprintln!(
127            "\nNote: {fallback_count} tensor(s) fell back to F32 (scalars/biases too \
128             small for Q8_0 blocks)."
129        );
130    }
131    println!("Writing {mapped} tensors to {} …", output_path.display());
132    let out_file = File::create(output_path)
133        .with_context(|| format!("create output file {}", output_path.display()))?;
134    let mut buf_writer = BufWriter::new(out_file);
135    writer.write_to(&mut buf_writer)?;
136    println!("Done.");
137
138    Ok(())
139}
140
141fn ggml_type_from_st(dtype: StDtype) -> anyhow::Result<GGMLType> {
142    match dtype {
143        StDtype::F32 => Ok(GGMLType::F32),
144        StDtype::F16 => Ok(GGMLType::F16),
145        StDtype::BF16 => Ok(GGMLType::BF16),
146        other => anyhow::bail!("unsupported safetensors dtype: {other:?}"),
147    }
148}
149
150/// Build GGUF metadata section from parsed config.
151fn write_metadata(writer: &mut GGUFWriter, model_id: &str, config: &Chronos2Config) {
152    let cc = &config.chronos_config;
153
154    writer.add_metadata(
155        "general.architecture",
156        GGUFMetaValue::String("chronos2".into()),
157    );
158    writer.add_metadata("general.name", GGUFMetaValue::String(model_id.into()));
159    writer.add_metadata(
160        "chronos2.block_count",
161        GGUFMetaValue::Uint32(config.num_layers),
162    );
163    writer.add_metadata(
164        "chronos2.embedding_length",
165        GGUFMetaValue::Uint32(config.d_model),
166    );
167    writer.add_metadata(
168        "chronos2.feed_forward_length",
169        GGUFMetaValue::Uint32(config.d_ff),
170    );
171    writer.add_metadata(
172        "chronos2.attention.head_count",
173        GGUFMetaValue::Uint32(config.num_heads),
174    );
175    writer.add_metadata(
176        "chronos2.attention.head_dim",
177        GGUFMetaValue::Uint32(config.d_kv),
178    );
179    writer.add_metadata(
180        "chronos2.rope_theta",
181        GGUFMetaValue::Float64(config.rope_theta),
182    );
183    writer.add_metadata(
184        "chronos2.layer_norm_epsilon",
185        GGUFMetaValue::Float64(config.layer_norm_epsilon),
186    );
187    writer.add_metadata(
188        "chronos2.context_length",
189        GGUFMetaValue::Uint32(cc.context_length),
190    );
191    writer.add_metadata(
192        "chronos2.patch_size",
193        GGUFMetaValue::Uint32(cc.input_patch_size),
194    );
195    writer.add_metadata(
196        "chronos2.patch_stride",
197        GGUFMetaValue::Uint32(cc.input_patch_stride),
198    );
199    writer.add_metadata(
200        "chronos2.quantile_count",
201        GGUFMetaValue::Uint32(cc.quantiles.len() as u32),
202    );
203    writer.add_metadata(
204        "chronos2.quantiles",
205        GGUFMetaValue::ArrayFloat32(cc.quantiles.clone()),
206    );
207    writer.add_metadata(
208        "chronos2.use_reg_token",
209        GGUFMetaValue::Bool(cc.use_reg_token),
210    );
211    writer.add_metadata("chronos2.use_arcsinh", GGUFMetaValue::Bool(cc.use_arcsinh));
212    writer.add_metadata(
213        "chronos2.time_encoding_scale",
214        GGUFMetaValue::Uint32(config.time_encoding_scale()),
215    );
216    writer.add_metadata(
217        "chronos2.dense_act_fn",
218        GGUFMetaValue::String(config.dense_act_fn().into()),
219    );
220}
221
222fn load_shard_bytes(shards: &[std::path::PathBuf]) -> anyhow::Result<Vec<Vec<u8>>> {
223    shards
224        .iter()
225        .map(|p| std::fs::read(p).with_context(|| format!("read shard {}", p.display())))
226        .collect()
227}
228
229/// Cast tensor bytes from `src_dtype` to `dst_dtype`.
230fn cast_data(data: &[u8], src: GGMLType, dst: GGMLType) -> anyhow::Result<Vec<u8>> {
231    if src == dst {
232        return Ok(data.to_vec());
233    }
234    if dst == GGMLType::Q8_0 {
235        let f32_values = decode_to_f32(data, src)?;
236        return quantize_q8_0(&f32_values);
237    }
238    match (src, dst) {
239        (GGMLType::F32, GGMLType::F16) => {
240            let f32_values = parse_f32_le(data)?;
241            let mut out = Vec::with_capacity(f32_values.len() * 2);
242            for v in f32_values {
243                let bits = f32_to_f16_bits(v);
244                out.extend_from_slice(&bits.to_le_bytes());
245            }
246            Ok(out)
247        }
248        (GGMLType::F32, GGMLType::BF16) => {
249            let f32_values = parse_f32_le(data)?;
250            let mut out = Vec::with_capacity(f32_values.len() * 2);
251            for v in f32_values {
252                let bits = (v.to_bits() >> 16) as u16;
253                out.extend_from_slice(&bits.to_le_bytes());
254            }
255            Ok(out)
256        }
257        (GGMLType::F16, GGMLType::BF16) => {
258            let mut out = Vec::with_capacity(data.len());
259            for chunk in data.chunks_exact(2) {
260                let f16_bits = u16::from_le_bytes([chunk[0], chunk[1]]);
261                let f32_val = f16_to_f32(f16_bits);
262                let bits = (f32_val.to_bits() >> 16) as u16;
263                out.extend_from_slice(&bits.to_le_bytes());
264            }
265            Ok(out)
266        }
267        (GGMLType::BF16, GGMLType::F32) => {
268            let mut out = Vec::with_capacity(data.len() * 2);
269            for chunk in data.chunks_exact(2) {
270                let bf16_bits = u16::from_le_bytes([chunk[0], chunk[1]]);
271                let f32_bits = (bf16_bits as u32) << 16;
272                out.extend_from_slice(&f32_bits.to_le_bytes());
273            }
274            Ok(out)
275        }
276        (GGMLType::BF16, GGMLType::F16) => {
277            let mut out = Vec::with_capacity(data.len());
278            for chunk in data.chunks_exact(2) {
279                let bf16_bits = u16::from_le_bytes([chunk[0], chunk[1]]);
280                let f32_bits = (bf16_bits as u32) << 16;
281                let f32_val = f32::from_bits(f32_bits);
282                let f16_bits = f32_to_f16_bits(f32_val);
283                out.extend_from_slice(&f16_bits.to_le_bytes());
284            }
285            Ok(out)
286        }
287        (GGMLType::F16, GGMLType::F32) => {
288            let mut out = Vec::with_capacity(data.len() * 2);
289            for chunk in data.chunks_exact(2) {
290                let f16_bits = u16::from_le_bytes([chunk[0], chunk[1]]);
291                let f32_val = f16_to_f32(f16_bits);
292                out.extend_from_slice(&f32_val.to_bits().to_le_bytes());
293            }
294            Ok(out)
295        }
296        _ => anyhow::bail!("unsupported cast: {src:?} → {dst:?}"),
297    }
298}
299
300fn decode_to_f32(data: &[u8], src: GGMLType) -> anyhow::Result<Vec<f32>> {
301    match src {
302        GGMLType::F32 => parse_f32_le(data),
303        GGMLType::F16 => data
304            .chunks_exact(2)
305            .map(|c| Ok(f16_to_f32(u16::from_le_bytes([c[0], c[1]]))))
306            .collect(),
307        GGMLType::BF16 => data
308            .chunks_exact(2)
309            .map(|c| {
310                let bf16_bits = u16::from_le_bytes([c[0], c[1]]);
311                Ok(f32::from_bits((bf16_bits as u32) << 16))
312            })
313            .collect(),
314        GGMLType::Q8_0 => anyhow::bail!("Q8_0 → Q8_0 re-quantization not supported as source"),
315    }
316}
317
318fn transpose_f32(data: &[f32], n_rows: usize, n_cols: usize) -> Vec<f32> {
319    let mut out = vec![0.0f32; n_rows * n_cols];
320    for r in 0..n_rows {
321        for c in 0..n_cols {
322            out[c * n_rows + r] = data[r * n_cols + c];
323        }
324    }
325    out
326}
327
328/// Quantize a slice of f32 values to GGML Q8_0 block format.
329/// Each block: `[f16 scale (2 bytes)][32 × i8 (32 bytes)]` = 34 bytes.
330fn quantize_q8_0(values: &[f32]) -> anyhow::Result<Vec<u8>> {
331    const BLOCK: usize = 32;
332    if values.len() % BLOCK != 0 {
333        anyhow::bail!(
334            "Q8_0 requires element count divisible by {BLOCK}, got {}",
335            values.len()
336        );
337    }
338    let n_blocks = values.len() / BLOCK;
339    let mut out = vec![0u8; n_blocks * 34];
340
341    for b in 0..n_blocks {
342        let blk = &values[b * BLOCK..(b + 1) * BLOCK];
343        let amax = blk.iter().copied().map(f32::abs).fold(0.0f32, f32::max);
344        let d = if amax == 0.0 { 0.0f32 } else { amax / 127.0 };
345        let d_inv = if d == 0.0 { 0.0f32 } else { 1.0 / d };
346
347        let base = b * 34;
348        let d_f16 = f32_to_f16_bits(d);
349        out[base..base + 2].copy_from_slice(&d_f16.to_le_bytes());
350        for i in 0..BLOCK {
351            let q = (blk[i] * d_inv).round().clamp(-127.0, 127.0) as i8;
352            out[base + 2 + i] = q as u8;
353        }
354    }
355    Ok(out)
356}
357
358fn parse_f32_le(data: &[u8]) -> anyhow::Result<Vec<f32>> {
359    if data.len() % 4 != 0 {
360        anyhow::bail!("f32 data length not divisible by 4");
361    }
362    Ok(data
363        .chunks_exact(4)
364        .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
365        .collect())
366}
367
368fn f32_to_f16_bits(v: f32) -> u16 {
369    let bits = v.to_bits();
370    let sign = ((bits >> 16) & 0x8000) as u16;
371    let exp = ((bits >> 23) & 0xFF) as i32;
372    let mantissa = bits & 0x007F_FFFF;
373
374    if exp == 0xFF {
375        return sign | 0x7C00 | if mantissa != 0 { 0x0200 } else { 0 };
376    }
377    let new_exp = exp - 127 + 15;
378    if new_exp >= 31 {
379        return sign | 0x7C00;
380    }
381    if new_exp <= 0 {
382        if new_exp < -10 {
383            return sign;
384        }
385        let m = (mantissa | 0x0080_0000) >> (1 - new_exp);
386        return sign | (m >> 13) as u16;
387    }
388    sign | ((new_exp as u16) << 10) | (mantissa >> 13) as u16
389}
390
391fn f16_to_f32(bits: u16) -> f32 {
392    let sign = ((bits & 0x8000) as u32) << 16;
393    let exp = ((bits >> 10) & 0x1F) as i32;
394    let mantissa = (bits & 0x03FF) as u32;
395
396    let f32_bits = if exp == 0 {
397        if mantissa == 0 {
398            sign
399        } else {
400            let mut m = mantissa;
401            let mut e = 0i32;
402            while m & 0x0400 == 0 {
403                m <<= 1;
404                e += 1;
405            }
406            sign | ((127 - 15 - e + 1) as u32) << 23 | (m & 0x03FF) << 13
407        }
408    } else if exp == 31 {
409        sign | 0x7F80_0000 | (mantissa << 13)
410    } else {
411        sign | ((exp + 127 - 15) as u32) << 23 | (mantissa << 13)
412    };
413    f32::from_bits(f32_bits)
414}