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::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()).with_context(|| {
67 format!(
68 "tensor {hf_name}: unsupported dtype {:?}",
69 tensor_view.dtype()
70 )
71 })?;
72
73 let raw_data = tensor_view.data();
74 let py_shape = tensor_view.shape();
75 let n_elems: usize = py_shape.iter().product();
76 let innermost = py_shape.last().copied().unwrap_or(1);
77 let _outermost = py_shape.first().copied().unwrap_or(1);
78
79 let (dst_dtype, gguf_shape, tensor_data) =
80 if opts.output_dtype == GGMLType::Q8_0 && (innermost % 32 != 0 || n_elems % 32 != 0) {
81 fallback_count += 1;
82 let data = cast_data(raw_data, src_dtype, GGMLType::F32)
83 .with_context(|| format!("tensor {hf_name}: cast failed"))?;
84 let gs = py_shape.iter().rev().map(|&d| d as u64).collect();
85 (GGMLType::F32, gs, data)
86 } else {
87 let dst = opts.output_dtype;
88 let data = cast_data(raw_data, src_dtype, dst)
89 .with_context(|| format!("tensor {hf_name}: cast failed"))?;
90 let gs = py_shape.iter().rev().map(|&d| d as u64).collect();
91 (dst, gs, data)
92 };
93
94 writer.add_tensor(gguf_name, gguf_shape, dst_dtype, tensor_data);
95 mapped += 1;
96 pb.inc(1);
97 }
98
99 pb.finish_with_message("tensors processed");
100
101 if !skipped.is_empty() {
102 eprintln!(
103 "\nWarning: {} tensor(s) skipped (unrecognised names):",
104 skipped.len()
105 );
106 for name in &skipped {
107 eprintln!(" {name}");
108 }
109 }
110 if fallback_count > 0 {
111 eprintln!(
112 "\nNote: {fallback_count} tensor(s) fell back to F32 (too small for Q8_0 blocks)."
113 );
114 }
115
116 println!("Writing {mapped} tensors to {} …", output_path.display());
117 let out_file =
118 File::create(output_path).with_context(|| format!("create {}", output_path.display()))?;
119 let mut buf = BufWriter::new(out_file);
120 writer.write_to(&mut buf)?;
121 println!("Done.");
122 Ok(())
123}
124
125fn write_metadata(writer: &mut GGUFWriter, model_id: &str, cfg: &SundialConfig) {
126 writer.add_metadata(
127 "general.architecture",
128 GGUFMetaValue::String("sundial1".into()),
129 );
130 writer.add_metadata("general.name", GGUFMetaValue::String(model_id.into()));
131 writer.add_metadata(
132 "sundial1.block_count",
133 GGUFMetaValue::Uint32(cfg.num_hidden_layers as u32),
134 );
135 writer.add_metadata(
136 "sundial1.embedding_length",
137 GGUFMetaValue::Uint32(cfg.hidden_size as u32),
138 );
139 writer.add_metadata(
140 "sundial1.feed_forward_length",
141 GGUFMetaValue::Uint32(cfg.intermediate_size as u32),
142 );
143 writer.add_metadata(
144 "sundial1.attention.head_count",
145 GGUFMetaValue::Uint32(cfg.num_attention_heads as u32),
146 );
147 writer.add_metadata(
148 "sundial1.attention.head_dim",
149 GGUFMetaValue::Uint32(cfg.head_dim() as u32),
150 );
151 writer.add_metadata(
152 "sundial1.input_token_len",
153 GGUFMetaValue::Uint32(cfg.input_token_len as u32),
154 );
155 writer.add_metadata(
156 "sundial1.output_token_len",
157 GGUFMetaValue::Uint32(cfg.output_token_len() as u32),
158 );
159 writer.add_metadata(
160 "sundial1.rope_theta",
161 GGUFMetaValue::Float64(cfg.rope_theta),
162 );
163 writer.add_metadata(
164 "sundial1.flow.depth",
165 GGUFMetaValue::Uint32(cfg.flow_loss_depth as u32),
166 );
167 writer.add_metadata(
168 "sundial1.flow.num_sampling_steps",
169 GGUFMetaValue::Uint32(cfg.num_sampling_steps as u32),
170 );
171}
172
173fn ggml_type_from_st(dtype: StDtype) -> anyhow::Result<GGMLType> {
174 match dtype {
175 StDtype::F32 => Ok(GGMLType::F32),
176 StDtype::F16 => Ok(GGMLType::F16),
177 StDtype::BF16 => Ok(GGMLType::BF16),
178 other => anyhow::bail!("unsupported safetensors dtype: {other:?}"),
179 }
180}
181
182fn cast_data(data: &[u8], src: GGMLType, dst: GGMLType) -> anyhow::Result<Vec<u8>> {
183 if src == dst {
184 return Ok(data.to_vec());
185 }
186 if dst == GGMLType::Q8_0 {
187 let f32_values = decode_to_f32(data, src)?;
188 return quantize_q8_0(&f32_values);
189 }
190 match (src, dst) {
191 (GGMLType::F32, GGMLType::F16) => {
192 let vals = parse_f32_le(data)?;
193 let mut out = Vec::with_capacity(vals.len() * 2);
194 for v in vals {
195 out.extend_from_slice(&f32_to_f16_bits(v).to_le_bytes());
196 }
197 Ok(out)
198 }
199 (GGMLType::F32, GGMLType::BF16) => {
200 let vals = parse_f32_le(data)?;
201 let mut out = Vec::with_capacity(vals.len() * 2);
202 for v in vals {
203 out.extend_from_slice(&((v.to_bits() >> 16) as u16).to_le_bytes());
204 }
205 Ok(out)
206 }
207 (GGMLType::F16, GGMLType::BF16) => {
208 let mut out = Vec::with_capacity(data.len());
209 for c in data.chunks_exact(2) {
210 let f32_val = f16_to_f32(u16::from_le_bytes([c[0], c[1]]));
211 out.extend_from_slice(&((f32_val.to_bits() >> 16) as u16).to_le_bytes());
212 }
213 Ok(out)
214 }
215 (GGMLType::BF16, GGMLType::F32) => {
216 let mut out = Vec::with_capacity(data.len() * 2);
217 for c in data.chunks_exact(2) {
218 let bf = u16::from_le_bytes([c[0], c[1]]);
219 out.extend_from_slice(&((bf as u32) << 16).to_le_bytes());
220 }
221 Ok(out)
222 }
223 (GGMLType::BF16, GGMLType::F16) => {
224 let mut out = Vec::with_capacity(data.len());
225 for c in data.chunks_exact(2) {
226 let bf = u16::from_le_bytes([c[0], c[1]]);
227 let f32_val = f32::from_bits((bf as u32) << 16);
228 out.extend_from_slice(&f32_to_f16_bits(f32_val).to_le_bytes());
229 }
230 Ok(out)
231 }
232 (GGMLType::F16, GGMLType::F32) => {
233 let mut out = Vec::with_capacity(data.len() * 2);
234 for c in data.chunks_exact(2) {
235 let f16 = u16::from_le_bytes([c[0], c[1]]);
236 out.extend_from_slice(&f16_to_f32(f16).to_bits().to_le_bytes());
237 }
238 Ok(out)
239 }
240 _ => anyhow::bail!("unsupported cast: {src:?} → {dst:?}"),
241 }
242}
243
244fn decode_to_f32(data: &[u8], src: GGMLType) -> anyhow::Result<Vec<f32>> {
245 match src {
246 GGMLType::F32 => parse_f32_le(data),
247 GGMLType::F16 => data
248 .chunks_exact(2)
249 .map(|c| Ok(f16_to_f32(u16::from_le_bytes([c[0], c[1]]))))
250 .collect(),
251 GGMLType::BF16 => data
252 .chunks_exact(2)
253 .map(|c| {
254 Ok(f32::from_bits(
255 (u16::from_le_bytes([c[0], c[1]]) as u32) << 16,
256 ))
257 })
258 .collect(),
259 GGMLType::Q8_0 => anyhow::bail!("Q8_0 re-quantization not supported"),
260 }
261}
262
263fn quantize_q8_0(values: &[f32]) -> anyhow::Result<Vec<u8>> {
264 const BLOCK: usize = 32;
265 if values.len() % BLOCK != 0 {
266 anyhow::bail!(
267 "Q8_0 requires elem count divisible by {BLOCK}, got {}",
268 values.len()
269 );
270 }
271 let n_blocks = values.len() / BLOCK;
272 let mut out = vec![0u8; n_blocks * 34];
273 for b in 0..n_blocks {
274 let blk = &values[b * BLOCK..(b + 1) * BLOCK];
275 let amax = blk.iter().copied().map(f32::abs).fold(0.0f32, f32::max);
276 let d = if amax == 0.0 { 0.0f32 } else { amax / 127.0 };
277 let d_inv = if d == 0.0 { 0.0f32 } else { 1.0 / d };
278 let base = b * 34;
279 out[base..base + 2].copy_from_slice(&f32_to_f16_bits(d).to_le_bytes());
280 for i in 0..BLOCK {
281 out[base + 2 + i] = (blk[i] * d_inv).round().clamp(-127.0, 127.0) as i8 as u8;
282 }
283 }
284 Ok(out)
285}
286
287fn parse_f32_le(data: &[u8]) -> anyhow::Result<Vec<f32>> {
288 if data.len() % 4 != 0 {
289 anyhow::bail!("f32 length not divisible by 4");
290 }
291 Ok(data
292 .chunks_exact(4)
293 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
294 .collect())
295}
296
297fn f32_to_f16_bits(v: f32) -> u16 {
298 let bits = v.to_bits();
299 let sign = ((bits >> 16) & 0x8000) as u16;
300 let exp = ((bits >> 23) & 0xFF) as i32;
301 let mantissa = bits & 0x007F_FFFF;
302 if exp == 0xFF {
303 return sign | 0x7C00 | if mantissa != 0 { 0x0200 } else { 0 };
304 }
305 let new_exp = exp - 127 + 15;
306 if new_exp >= 31 {
307 return sign | 0x7C00;
308 }
309 if new_exp <= 0 {
310 if new_exp < -10 {
311 return sign;
312 }
313 let m = (mantissa | 0x0080_0000) >> (1 - new_exp);
314 return sign | (m >> 13) as u16;
315 }
316 sign | ((new_exp as u16) << 10) | (mantissa >> 13) as u16
317}
318
319fn f16_to_f32(bits: u16) -> f32 {
320 let sign = ((bits & 0x8000) as u32) << 16;
321 let exp = ((bits >> 10) & 0x1F) as i32;
322 let mantissa = (bits & 0x03FF) as u32;
323 let f32_bits = if exp == 0 {
324 if mantissa == 0 {
325 sign
326 } else {
327 let mut m = mantissa;
328 let mut e = 0i32;
329 while m & 0x0400 == 0 {
330 m <<= 1;
331 e += 1;
332 }
333 sign | ((127 - 15 - e + 1) as u32) << 23 | (m & 0x03FF) << 13
334 }
335 } else if exp == 31 {
336 sign | 0x7F80_0000 | (mantissa << 13)
337 } else {
338 sign | ((exp + 127 - 15) as u32) << 23 | (mantissa << 13)
339 };
340 f32::from_bits(f32_bits)
341}