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
16pub struct ConvertOptions {
18 pub output_dtype: GGMLType,
19}
20
21pub 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 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
150fn 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
229fn 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
328fn 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}