Skip to main content

zsfm_toto/
convert.rs

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