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
15pub struct ConvertOptions {
17 pub output_dtype: GGMLType,
18}
19
20pub 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 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
138fn 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
198fn 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
287fn 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}