Skip to main content

zsfm_sundial/
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;
8use safetensors::Dtype as StDtype;
9
10use zsfm_gguf::{GGMLType, GGUFMetaValue, GGUFWriter};
11use zsfm_hub::ModelFiles;
12
13use crate::config::SundialConfig;
14use crate::tensor_map::map_tensor_name;
15
16pub struct ConvertOptions {
17    pub output_dtype: GGMLType,
18}
19
20pub fn convert(
21    model_id: &str,
22    files: &ModelFiles,
23    config: &SundialConfig,
24    opts: &ConvertOptions,
25    output_path: &Path,
26) -> anyhow::Result<()> {
27    let mut writer = GGUFWriter::new();
28    write_metadata(&mut writer, model_id, config);
29
30    let safetensors_path = files
31        .safetensors_shards
32        .first()
33        .context("no safetensors file downloaded")?;
34    let raw = std::fs::read(safetensors_path)
35        .with_context(|| format!("read {}", safetensors_path.display()))?;
36    let tensors = SafeTensors::deserialize(&raw).context("deserialize safetensors")?;
37
38    let total = tensors.len();
39    println!("Found {total} tensors.");
40
41    let pb = ProgressBar::new(total as u64);
42    pb.set_style(
43        ProgressStyle::with_template(
44            "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}",
45        )
46        .unwrap()
47        .progress_chars("=>-"),
48    );
49
50    let mut mapped = 0usize;
51    let mut skipped: Vec<String> = Vec::new();
52    let mut fallback_count = 0usize;
53
54    for (hf_name, tensor_view) in tensors.tensors() {
55        pb.set_message(hf_name.to_string());
56
57        let gguf_name = match map_tensor_name(&hf_name) {
58            Some(n) => n,
59            None => {
60                skipped.push(hf_name.to_string());
61                pb.inc(1);
62                continue;
63            }
64        };
65
66        let src_dtype = ggml_type_from_st(tensor_view.dtype())
67            .with_context(|| format!("tensor {hf_name}: unsupported dtype {:?}", tensor_view.dtype()))?;
68
69        let raw_data = tensor_view.data();
70        let py_shape = tensor_view.shape();
71        let n_elems: usize = py_shape.iter().product();
72        let innermost = py_shape.last().copied().unwrap_or(1);
73        let _outermost = py_shape.first().copied().unwrap_or(1);
74
75        let (dst_dtype, gguf_shape, tensor_data) =
76            if opts.output_dtype == GGMLType::Q8_0 && (innermost % 32 != 0 || n_elems % 32 != 0) {
77                fallback_count += 1;
78                let data = cast_data(raw_data, src_dtype, GGMLType::F32)
79                    .with_context(|| format!("tensor {hf_name}: cast failed"))?;
80                let gs = py_shape.iter().rev().map(|&d| d as u64).collect();
81                (GGMLType::F32, gs, data)
82            } else {
83                let dst = opts.output_dtype;
84                let data = cast_data(raw_data, src_dtype, dst)
85                    .with_context(|| format!("tensor {hf_name}: cast failed"))?;
86                let gs = py_shape.iter().rev().map(|&d| d as u64).collect();
87                (dst, gs, data)
88            };
89
90        writer.add_tensor(gguf_name, gguf_shape, dst_dtype, tensor_data);
91        mapped += 1;
92        pb.inc(1);
93    }
94
95    pb.finish_with_message("tensors processed");
96
97    if !skipped.is_empty() {
98        eprintln!("\nWarning: {} tensor(s) skipped (unrecognised names):", skipped.len());
99        for name in &skipped {
100            eprintln!("  {name}");
101        }
102    }
103    if fallback_count > 0 {
104        eprintln!("\nNote: {fallback_count} tensor(s) fell back to F32 (too small for Q8_0 blocks).");
105    }
106
107    println!("Writing {mapped} tensors to {} …", output_path.display());
108    let out_file = File::create(output_path)
109        .with_context(|| format!("create {}", output_path.display()))?;
110    let mut buf = BufWriter::new(out_file);
111    writer.write_to(&mut buf)?;
112    println!("Done.");
113    Ok(())
114}
115
116fn write_metadata(writer: &mut GGUFWriter, model_id: &str, cfg: &SundialConfig) {
117    writer.add_metadata("general.architecture",            GGUFMetaValue::String("sundial1".into()));
118    writer.add_metadata("general.name",                    GGUFMetaValue::String(model_id.into()));
119    writer.add_metadata("sundial1.block_count",            GGUFMetaValue::Uint32(cfg.num_hidden_layers as u32));
120    writer.add_metadata("sundial1.embedding_length",       GGUFMetaValue::Uint32(cfg.hidden_size as u32));
121    writer.add_metadata("sundial1.feed_forward_length",    GGUFMetaValue::Uint32(cfg.intermediate_size as u32));
122    writer.add_metadata("sundial1.attention.head_count",   GGUFMetaValue::Uint32(cfg.num_attention_heads as u32));
123    writer.add_metadata("sundial1.attention.head_dim",     GGUFMetaValue::Uint32(cfg.head_dim() as u32));
124    writer.add_metadata("sundial1.input_token_len",        GGUFMetaValue::Uint32(cfg.input_token_len as u32));
125    writer.add_metadata("sundial1.output_token_len",       GGUFMetaValue::Uint32(cfg.output_token_len() as u32));
126    writer.add_metadata("sundial1.rope_theta",             GGUFMetaValue::Float64(cfg.rope_theta));
127    writer.add_metadata("sundial1.flow.depth",             GGUFMetaValue::Uint32(cfg.flow_loss_depth as u32));
128    writer.add_metadata("sundial1.flow.num_sampling_steps",GGUFMetaValue::Uint32(cfg.num_sampling_steps as u32));
129}
130
131fn ggml_type_from_st(dtype: StDtype) -> anyhow::Result<GGMLType> {
132    match dtype {
133        StDtype::F32  => Ok(GGMLType::F32),
134        StDtype::F16  => Ok(GGMLType::F16),
135        StDtype::BF16 => Ok(GGMLType::BF16),
136        other => anyhow::bail!("unsupported safetensors dtype: {other:?}"),
137    }
138}
139
140fn cast_data(data: &[u8], src: GGMLType, dst: GGMLType) -> anyhow::Result<Vec<u8>> {
141    if src == dst { return Ok(data.to_vec()); }
142    if dst == GGMLType::Q8_0 {
143        let f32_values = decode_to_f32(data, src)?;
144        return quantize_q8_0(&f32_values);
145    }
146    match (src, dst) {
147        (GGMLType::F32, GGMLType::F16) => {
148            let vals = parse_f32_le(data)?;
149            let mut out = Vec::with_capacity(vals.len() * 2);
150            for v in vals { out.extend_from_slice(&f32_to_f16_bits(v).to_le_bytes()); }
151            Ok(out)
152        }
153        (GGMLType::F32, GGMLType::BF16) => {
154            let vals = parse_f32_le(data)?;
155            let mut out = Vec::with_capacity(vals.len() * 2);
156            for v in vals { out.extend_from_slice(&((v.to_bits() >> 16) as u16).to_le_bytes()); }
157            Ok(out)
158        }
159        (GGMLType::F16, GGMLType::BF16) => {
160            let mut out = Vec::with_capacity(data.len());
161            for c in data.chunks_exact(2) {
162                let f32_val = f16_to_f32(u16::from_le_bytes([c[0], c[1]]));
163                out.extend_from_slice(&((f32_val.to_bits() >> 16) as u16).to_le_bytes());
164            }
165            Ok(out)
166        }
167        (GGMLType::BF16, GGMLType::F32) => {
168            let mut out = Vec::with_capacity(data.len() * 2);
169            for c in data.chunks_exact(2) {
170                let bf = u16::from_le_bytes([c[0], c[1]]);
171                out.extend_from_slice(&((bf as u32) << 16).to_le_bytes());
172            }
173            Ok(out)
174        }
175        (GGMLType::BF16, GGMLType::F16) => {
176            let mut out = Vec::with_capacity(data.len());
177            for c in data.chunks_exact(2) {
178                let bf = u16::from_le_bytes([c[0], c[1]]);
179                let f32_val = f32::from_bits((bf as u32) << 16);
180                out.extend_from_slice(&f32_to_f16_bits(f32_val).to_le_bytes());
181            }
182            Ok(out)
183        }
184        (GGMLType::F16, GGMLType::F32) => {
185            let mut out = Vec::with_capacity(data.len() * 2);
186            for c in data.chunks_exact(2) {
187                let f16 = u16::from_le_bytes([c[0], c[1]]);
188                out.extend_from_slice(&f16_to_f32(f16).to_bits().to_le_bytes());
189            }
190            Ok(out)
191        }
192        _ => anyhow::bail!("unsupported cast: {src:?} → {dst:?}"),
193    }
194}
195
196fn decode_to_f32(data: &[u8], src: GGMLType) -> anyhow::Result<Vec<f32>> {
197    match src {
198        GGMLType::F32  => parse_f32_le(data),
199        GGMLType::F16  => data.chunks_exact(2)
200            .map(|c| Ok(f16_to_f32(u16::from_le_bytes([c[0], c[1]]))))
201            .collect(),
202        GGMLType::BF16 => data.chunks_exact(2)
203            .map(|c| Ok(f32::from_bits((u16::from_le_bytes([c[0], c[1]]) as u32) << 16)))
204            .collect(),
205        GGMLType::Q8_0 => anyhow::bail!("Q8_0 re-quantization not supported"),
206    }
207}
208
209fn quantize_q8_0(values: &[f32]) -> anyhow::Result<Vec<u8>> {
210    const BLOCK: usize = 32;
211    if values.len() % BLOCK != 0 {
212        anyhow::bail!("Q8_0 requires elem count divisible by {BLOCK}, got {}", values.len());
213    }
214    let n_blocks = values.len() / BLOCK;
215    let mut out = vec![0u8; n_blocks * 34];
216    for b in 0..n_blocks {
217        let blk = &values[b * BLOCK..(b + 1) * BLOCK];
218        let amax = blk.iter().copied().map(f32::abs).fold(0.0f32, f32::max);
219        let d = if amax == 0.0 { 0.0f32 } else { amax / 127.0 };
220        let d_inv = if d == 0.0 { 0.0f32 } else { 1.0 / d };
221        let base = b * 34;
222        out[base..base + 2].copy_from_slice(&f32_to_f16_bits(d).to_le_bytes());
223        for i in 0..BLOCK {
224            out[base + 2 + i] = (blk[i] * d_inv).round().clamp(-127.0, 127.0) as i8 as u8;
225        }
226    }
227    Ok(out)
228}
229
230fn parse_f32_le(data: &[u8]) -> anyhow::Result<Vec<f32>> {
231    if data.len() % 4 != 0 { anyhow::bail!("f32 length not divisible by 4"); }
232    Ok(data.chunks_exact(4)
233        .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
234        .collect())
235}
236
237fn f32_to_f16_bits(v: f32) -> u16 {
238    let bits = v.to_bits();
239    let sign = ((bits >> 16) & 0x8000) as u16;
240    let exp = ((bits >> 23) & 0xFF) as i32;
241    let mantissa = bits & 0x007F_FFFF;
242    if exp == 0xFF { return sign | 0x7C00 | if mantissa != 0 { 0x0200 } else { 0 }; }
243    let new_exp = exp - 127 + 15;
244    if new_exp >= 31 { return sign | 0x7C00; }
245    if new_exp <= 0 {
246        if new_exp < -10 { return sign; }
247        let m = (mantissa | 0x0080_0000) >> (1 - new_exp);
248        return sign | (m >> 13) as u16;
249    }
250    sign | ((new_exp as u16) << 10) | (mantissa >> 13) as u16
251}
252
253fn f16_to_f32(bits: u16) -> f32 {
254    let sign = ((bits & 0x8000) as u32) << 16;
255    let exp = ((bits >> 10) & 0x1F) as i32;
256    let mantissa = (bits & 0x03FF) as u32;
257    let f32_bits = if exp == 0 {
258        if mantissa == 0 { sign }
259        else {
260            let mut m = mantissa; let mut e = 0i32;
261            while m & 0x0400 == 0 { m <<= 1; e += 1; }
262            sign | ((127 - 15 - e + 1) as u32) << 23 | (m & 0x03FF) << 13
263        }
264    } else if exp == 31 { sign | 0x7F80_0000 | (mantissa << 13) }
265    else { sign | ((exp + 127 - 15) as u32) << 23 | (mantissa << 13) };
266    f32::from_bits(f32_bits)
267}