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
17pub struct ConvertOptions {
19 pub output_dtype: GGMLType,
21}
22
23pub 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 write_metadata(&mut writer, model_id, config);
35
36 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(); 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 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 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 let gs = py_shape.iter().map(|&d| d as u64).collect();
107 (GGMLType::Q8_0, gs, qdata)
108 } else {
109 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 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
175fn 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
209fn 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
217fn 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 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 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
293fn 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
312fn 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
323fn 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
355fn 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
366fn f32_to_f16_bits(v: f32) -> u16 {
368 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 };
377 }
378
379 let new_exp = exp - 127 + 15;
380 if new_exp >= 31 {
381 return sign | 0x7C00; }
383 if new_exp <= 0 {
384 if new_exp < -10 {
385 return sign; }
387 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
394fn 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 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}