Skip to main content

zsfm_tabfm/
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};
11
12use crate::config::TabFMConfig;
13use crate::tensor_map::map_tensor_name;
14
15/// Options that control how tensors are stored in the output GGUF.
16pub struct ConvertOptions {
17    pub output_dtype: GGMLType,
18}
19
20/// Read a single (already `pytorch_model.bin` -> safetensors converted) checkpoint, convert
21/// every tensor, and write a GGUF file to `output_path`.
22pub fn convert(
23    model_id: &str,
24    safetensors_path: &Path,
25    config: &TabFMConfig,
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 bytes = std::fs::read(safetensors_path)
34        .with_context(|| format!("read {}", safetensors_path.display()))?;
35    let view = SafeTensors::deserialize(&bytes).context("deserialize safetensors")?;
36
37    let total_tensors = view.len();
38    println!("Found {total_tensors} tensors.");
39
40    let pb = ProgressBar::new(total_tensors as u64);
41    pb.set_style(
42        ProgressStyle::with_template(
43            "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}",
44        )
45        .unwrap()
46        .progress_chars("=>-"),
47    );
48
49    let mut mapped = 0usize;
50    let mut skipped: Vec<String> = Vec::new();
51    let mut fallback_count = 0usize;
52
53    for (hf_name, tensor_view) in view.tensors() {
54        pb.set_message(hf_name.to_string());
55
56        let gguf_name = match map_tensor_name(&hf_name) {
57            Some(n) => n,
58            None => {
59                skipped.push(hf_name.to_string());
60                pb.inc(1);
61                continue;
62            }
63        };
64
65        let src_dtype = ggml_type_from_st(tensor_view.dtype()).with_context(|| {
66            format!(
67                "tensor {hf_name}: unsupported dtype {:?}",
68                tensor_view.dtype()
69            )
70        })?;
71
72        let raw_data = tensor_view.data();
73        let py_shape = tensor_view.shape();
74        let n_elems: usize = py_shape.iter().product();
75        let innermost = py_shape.last().copied().unwrap_or(1);
76
77        // Q8_0: Q8_0 blocks need the innermost dim (last Python dim, first GGUF dim) to be
78        // divisible by 32. When it isn't, fall back to F32 for that tensor (biases, 1-D
79        // buffers like fourier frequencies / RoPE freqs / per_dim_scale, etc).
80        let (dst_dtype, gguf_shape, tensor_data) =
81            if opts.output_dtype == GGMLType::Q8_0 && (innermost % 32 != 0 || n_elems % 32 != 0) {
82                fallback_count += 1;
83                let data = cast_data(raw_data, src_dtype, GGMLType::F32)
84                    .with_context(|| format!("tensor {hf_name}: cast failed"))?;
85                let gs = py_shape.iter().rev().map(|&d| d as u64).collect();
86                (GGMLType::F32, gs, data)
87            } else {
88                let dst = opts.output_dtype;
89                let data = cast_data(raw_data, src_dtype, dst)
90                    .with_context(|| format!("tensor {hf_name}: cast failed"))?;
91                let gs = py_shape.iter().rev().map(|&d| d as u64).collect();
92                (dst, gs, data)
93            };
94
95        writer.add_tensor(gguf_name, gguf_shape, dst_dtype, tensor_data);
96        mapped += 1;
97        pb.inc(1);
98    }
99
100    pb.finish_with_message("tensors processed");
101
102    if !skipped.is_empty() {
103        eprintln!(
104            "\nWarning: {} tensor(s) had unrecognised names and were skipped:",
105            skipped.len()
106        );
107        for name in &skipped {
108            eprintln!("  {name}");
109        }
110        eprintln!("Update tensor_map.rs to include these if needed.");
111    }
112
113    if fallback_count > 0 {
114        eprintln!(
115            "\nNote: {fallback_count} tensor(s) fell back to F32 (scalars/biases/buffers too \
116             small for Q8_0 blocks)."
117        );
118    }
119    println!("Writing {mapped} tensors to {} …", output_path.display());
120    let out_file = File::create(output_path)
121        .with_context(|| format!("create output file {}", output_path.display()))?;
122    let mut buf_writer = BufWriter::new(out_file);
123    writer.write_to(&mut buf_writer)?;
124    println!("Done.");
125
126    Ok(())
127}
128
129fn ggml_type_from_st(dtype: StDtype) -> anyhow::Result<GGMLType> {
130    match dtype {
131        StDtype::F32 => Ok(GGMLType::F32),
132        StDtype::F16 => Ok(GGMLType::F16),
133        StDtype::BF16 => Ok(GGMLType::BF16),
134        other => anyhow::bail!("unsupported safetensors dtype: {other:?}"),
135    }
136}
137
138/// Build GGUF metadata section from parsed config.
139fn write_metadata(writer: &mut GGUFWriter, model_id: &str, config: &TabFMConfig) {
140    writer.add_metadata(
141        "general.architecture",
142        GGUFMetaValue::String("tabfm".into()),
143    );
144    writer.add_metadata("general.name", GGUFMetaValue::String(model_id.into()));
145    writer.add_metadata(
146        "tabfm.is_classifier",
147        GGUFMetaValue::Bool(config.is_classifier),
148    );
149    writer.add_metadata(
150        "tabfm.embedding_length",
151        GGUFMetaValue::Uint32(config.embed_dim),
152    );
153    writer.add_metadata(
154        "tabfm.max_classes",
155        GGUFMetaValue::Uint32(config.max_classes),
156    );
157    writer.add_metadata(
158        "tabfm.col_block_count",
159        GGUFMetaValue::Uint32(config.col_num_blocks),
160    );
161    writer.add_metadata(
162        "tabfm.col_head_count",
163        GGUFMetaValue::Uint32(config.col_nhead),
164    );
165    writer.add_metadata(
166        "tabfm.col_num_inds",
167        GGUFMetaValue::Uint32(config.col_num_inds),
168    );
169    writer.add_metadata(
170        "tabfm.row_block_count",
171        GGUFMetaValue::Uint32(config.row_num_blocks),
172    );
173    writer.add_metadata(
174        "tabfm.row_head_count",
175        GGUFMetaValue::Uint32(config.row_nhead),
176    );
177    writer.add_metadata(
178        "tabfm.row_num_cls",
179        GGUFMetaValue::Uint32(config.row_num_cls),
180    );
181    writer.add_metadata(
182        "tabfm.icl_block_count",
183        GGUFMetaValue::Uint32(config.icl_num_blocks),
184    );
185    writer.add_metadata(
186        "tabfm.icl_head_count",
187        GGUFMetaValue::Uint32(config.icl_nhead),
188    );
189    writer.add_metadata("tabfm.ff_factor", GGUFMetaValue::Uint32(config.ff_factor));
190    writer.add_metadata(
191        "tabfm.feature_group_size",
192        GGUFMetaValue::Uint32(config.feature_group_size),
193    );
194    writer.add_metadata("tabfm.num_freq", GGUFMetaValue::Uint32(config.num_freq));
195    writer.add_metadata("tabfm.norm_eps", GGUFMetaValue::Float64(config.norm_eps));
196}
197
198/// Cast tensor bytes from `src_dtype` to `dst_dtype`.
199fn cast_data(data: &[u8], src: GGMLType, dst: GGMLType) -> anyhow::Result<Vec<u8>> {
200    if src == dst {
201        return Ok(data.to_vec());
202    }
203    if dst == GGMLType::Q8_0 {
204        let f32_values = decode_to_f32(data, src)?;
205        return quantize_q8_0(&f32_values);
206    }
207    match (src, dst) {
208        (GGMLType::F32, GGMLType::F16) => {
209            let f32_values = parse_f32_le(data)?;
210            let mut out = Vec::with_capacity(f32_values.len() * 2);
211            for v in f32_values {
212                let bits = f32_to_f16_bits(v);
213                out.extend_from_slice(&bits.to_le_bytes());
214            }
215            Ok(out)
216        }
217        (GGMLType::F32, GGMLType::BF16) => {
218            let f32_values = parse_f32_le(data)?;
219            let mut out = Vec::with_capacity(f32_values.len() * 2);
220            for v in f32_values {
221                let bits = (v.to_bits() >> 16) as u16;
222                out.extend_from_slice(&bits.to_le_bytes());
223            }
224            Ok(out)
225        }
226        (GGMLType::F16, GGMLType::BF16) => {
227            let mut out = Vec::with_capacity(data.len());
228            for chunk in data.chunks_exact(2) {
229                let f16_bits = u16::from_le_bytes([chunk[0], chunk[1]]);
230                let f32_val = f16_to_f32(f16_bits);
231                let bits = (f32_val.to_bits() >> 16) as u16;
232                out.extend_from_slice(&bits.to_le_bytes());
233            }
234            Ok(out)
235        }
236        (GGMLType::BF16, GGMLType::F32) => {
237            let mut out = Vec::with_capacity(data.len() * 2);
238            for chunk in data.chunks_exact(2) {
239                let bf16_bits = u16::from_le_bytes([chunk[0], chunk[1]]);
240                let f32_bits = (bf16_bits as u32) << 16;
241                out.extend_from_slice(&f32_bits.to_le_bytes());
242            }
243            Ok(out)
244        }
245        (GGMLType::BF16, GGMLType::F16) => {
246            let mut out = Vec::with_capacity(data.len());
247            for chunk in data.chunks_exact(2) {
248                let bf16_bits = u16::from_le_bytes([chunk[0], chunk[1]]);
249                let f32_bits = (bf16_bits as u32) << 16;
250                let f32_val = f32::from_bits(f32_bits);
251                let f16_bits = f32_to_f16_bits(f32_val);
252                out.extend_from_slice(&f16_bits.to_le_bytes());
253            }
254            Ok(out)
255        }
256        (GGMLType::F16, GGMLType::F32) => {
257            let mut out = Vec::with_capacity(data.len() * 2);
258            for chunk in data.chunks_exact(2) {
259                let f16_bits = u16::from_le_bytes([chunk[0], chunk[1]]);
260                let f32_val = f16_to_f32(f16_bits);
261                out.extend_from_slice(&f32_val.to_bits().to_le_bytes());
262            }
263            Ok(out)
264        }
265        _ => anyhow::bail!("unsupported cast: {src:?} → {dst:?}"),
266    }
267}
268
269fn decode_to_f32(data: &[u8], src: GGMLType) -> anyhow::Result<Vec<f32>> {
270    match src {
271        GGMLType::F32 => parse_f32_le(data),
272        GGMLType::F16 => data
273            .chunks_exact(2)
274            .map(|c| Ok(f16_to_f32(u16::from_le_bytes([c[0], c[1]]))))
275            .collect(),
276        GGMLType::BF16 => data
277            .chunks_exact(2)
278            .map(|c| {
279                let bf16_bits = u16::from_le_bytes([c[0], c[1]]);
280                Ok(f32::from_bits((bf16_bits as u32) << 16))
281            })
282            .collect(),
283        GGMLType::Q8_0 => anyhow::bail!("Q8_0 → Q8_0 re-quantization not supported as source"),
284    }
285}
286
287/// Quantize a slice of f32 values to GGML Q8_0 block format.
288/// Each block: `[f16 scale (2 bytes)][32 × i8 (32 bytes)]` = 34 bytes.
289fn quantize_q8_0(values: &[f32]) -> anyhow::Result<Vec<u8>> {
290    const BLOCK: usize = 32;
291    if values.len() % BLOCK != 0 {
292        anyhow::bail!(
293            "Q8_0 requires element count divisible by {BLOCK}, got {}",
294            values.len()
295        );
296    }
297    let n_blocks = values.len() / BLOCK;
298    let mut out = vec![0u8; n_blocks * 34];
299
300    for b in 0..n_blocks {
301        let blk = &values[b * BLOCK..(b + 1) * BLOCK];
302        let amax = blk.iter().copied().map(f32::abs).fold(0.0f32, f32::max);
303        let d = if amax == 0.0 { 0.0f32 } else { amax / 127.0 };
304        let d_inv = if d == 0.0 { 0.0f32 } else { 1.0 / d };
305
306        let base = b * 34;
307        let d_f16 = f32_to_f16_bits(d);
308        out[base..base + 2].copy_from_slice(&d_f16.to_le_bytes());
309        for i in 0..BLOCK {
310            let q = (blk[i] * d_inv).round().clamp(-127.0, 127.0) as i8;
311            out[base + 2 + i] = q as u8;
312        }
313    }
314    Ok(out)
315}
316
317fn parse_f32_le(data: &[u8]) -> anyhow::Result<Vec<f32>> {
318    if data.len() % 4 != 0 {
319        anyhow::bail!("f32 data length not divisible by 4");
320    }
321    Ok(data
322        .chunks_exact(4)
323        .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
324        .collect())
325}
326
327fn f32_to_f16_bits(v: f32) -> u16 {
328    let bits = v.to_bits();
329    let sign = ((bits >> 16) & 0x8000) as u16;
330    let exp = ((bits >> 23) & 0xFF) as i32;
331    let mantissa = bits & 0x007F_FFFF;
332
333    if exp == 0xFF {
334        return sign | 0x7C00 | if mantissa != 0 { 0x0200 } else { 0 };
335    }
336    let new_exp = exp - 127 + 15;
337    if new_exp >= 31 {
338        return sign | 0x7C00;
339    }
340    if new_exp <= 0 {
341        if new_exp < -10 {
342            return sign;
343        }
344        let m = (mantissa | 0x0080_0000) >> (1 - new_exp);
345        return sign | (m >> 13) as u16;
346    }
347    sign | ((new_exp as u16) << 10) | (mantissa >> 13) as u16
348}
349
350fn f16_to_f32(bits: u16) -> f32 {
351    let sign = ((bits & 0x8000) as u32) << 16;
352    let exp = ((bits >> 10) & 0x1F) as i32;
353    let mantissa = (bits & 0x03FF) as u32;
354
355    let f32_bits = if exp == 0 {
356        if mantissa == 0 {
357            sign
358        } else {
359            let mut m = mantissa;
360            let mut e = 0i32;
361            while m & 0x0400 == 0 {
362                m <<= 1;
363                e += 1;
364            }
365            sign | ((127 - 15 - e + 1) as u32) << 23 | (m & 0x03FF) << 13
366        }
367    } else if exp == 31 {
368        sign | 0x7F80_0000 | (mantissa << 13)
369    } else {
370        sign | ((exp + 127 - 15) as u32) << 23 | (mantissa << 13)
371    };
372    f32::from_bits(f32_bits)
373}