1use std::fs::File;
2use std::io::BufWriter;
3use std::path::{Path, PathBuf};
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::MomentConfig;
13use crate::tensor_map::map_tensor_name;
14
15pub struct ConvertOptions {
16 pub output_dtype: GGMLType,
17}
18
19pub fn convert(
20 shard_paths: &[PathBuf],
21 config: &MomentConfig,
22 opts: &ConvertOptions,
23 output_path: &Path,
24) -> anyhow::Result<()> {
25 let mut writer = GGUFWriter::new();
26 write_metadata(&mut writer, config);
27
28 let shard_bytes: Vec<Vec<u8>> = shard_paths
29 .iter()
30 .map(|p| std::fs::read(p).with_context(|| format!("read {}", p.display())))
31 .collect::<anyhow::Result<_>>()?;
32 let shard_views: Vec<SafeTensors> = shard_bytes
33 .iter()
34 .map(|b| SafeTensors::deserialize(b).context("deserialize shard"))
35 .collect::<anyhow::Result<_>>()?;
36
37 let total: usize = shard_views.iter().map(|s| s.len()).sum();
38 let pb = ProgressBar::new(total as u64);
39 pb.set_style(
40 ProgressStyle::with_template(
41 "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}",
42 )
43 .unwrap()
44 .progress_chars("=>-"),
45 );
46
47 let mut mapped = 0usize;
48 let mut skipped: Vec<String> = Vec::new();
49 let mut fallback_count = 0usize;
50
51 for shard in &shard_views {
52 for (hf_name, tensor_view) in shard.tensors() {
53 pb.set_message(hf_name.to_string());
54
55 let gguf_name = match map_tensor_name(&hf_name) {
56 Some(n) => n,
57 None => {
58 skipped.push(hf_name.to_string());
59 pb.inc(1);
60 continue;
61 }
62 };
63
64 let src_dtype = ggml_type_from_st(tensor_view.dtype()).with_context(|| {
65 format!(
66 "tensor {hf_name}: unsupported dtype {:?}",
67 tensor_view.dtype()
68 )
69 })?;
70
71 let raw_data = tensor_view.data();
72 let py_shape = tensor_view.shape();
73 let n_elems: usize = py_shape.iter().product();
74 let innermost = py_shape.last().copied().unwrap_or(1);
75 let _outermost = py_shape.first().copied().unwrap_or(1);
76
77 let (dst_dtype, gguf_shape, tensor_data) = if opts.output_dtype == GGMLType::Q8_0
78 && (innermost % 32 != 0 || n_elems % 32 != 0)
79 {
80 fallback_count += 1;
81 let data = cast_data(raw_data, src_dtype, GGMLType::F32)
82 .with_context(|| format!("tensor {hf_name}: cast failed"))?;
83 let gs = py_shape.iter().rev().map(|&d| d as u64).collect();
84 (GGMLType::F32, gs, data)
85 } else {
86 let dst = opts.output_dtype;
87 let data = cast_data(raw_data, src_dtype, dst)
88 .with_context(|| format!("tensor {hf_name}: cast failed"))?;
89 let gs = py_shape.iter().rev().map(|&d| d as u64).collect();
90 (dst, gs, data)
91 };
92
93 writer.add_tensor(gguf_name, gguf_shape, dst_dtype, tensor_data);
94 mapped += 1;
95 pb.inc(1);
96 }
97 } pb.finish_with_message("tensors processed");
100
101 if !skipped.is_empty() {
102 eprintln!("\nWarning: {} tensor(s) skipped:", skipped.len());
103 for name in &skipped {
104 eprintln!(" {name}");
105 }
106 }
107 if fallback_count > 0 {
108 eprintln!("\nNote: {fallback_count} tensor(s) fell back to F32.");
109 }
110
111 println!("Writing {mapped} tensors to {} …", output_path.display());
112 let out_file =
113 File::create(output_path).with_context(|| format!("create {}", output_path.display()))?;
114 let mut buf_writer = BufWriter::new(out_file);
115 writer.write_to(&mut buf_writer)?;
116 println!("Done.");
117 Ok(())
118}
119
120fn write_metadata(writer: &mut GGUFWriter, config: &MomentConfig) {
121 writer.add_metadata(
122 "general.architecture",
123 GGUFMetaValue::String("moment".into()),
124 );
125 writer.add_metadata(
126 "general.name",
127 GGUFMetaValue::String("MOMENT-1-large".into()),
128 );
129 writer.add_metadata(
130 "moment.d_model",
131 GGUFMetaValue::Uint32(config.d_model as u32),
132 );
133 writer.add_metadata(
134 "moment.n_layers",
135 GGUFMetaValue::Uint32(config.n_layers as u32),
136 );
137 writer.add_metadata(
138 "moment.n_heads",
139 GGUFMetaValue::Uint32(config.n_heads as u32),
140 );
141 writer.add_metadata(
142 "moment.head_dim",
143 GGUFMetaValue::Uint32(config.head_dim as u32),
144 );
145 writer.add_metadata("moment.d_ff", GGUFMetaValue::Uint32(config.d_ff as u32));
146 writer.add_metadata(
147 "moment.seq_len",
148 GGUFMetaValue::Uint32(config.seq_len as u32),
149 );
150 writer.add_metadata(
151 "moment.patch_len",
152 GGUFMetaValue::Uint32(config.patch_len as u32),
153 );
154 writer.add_metadata(
155 "moment.patch_stride",
156 GGUFMetaValue::Uint32(config.patch_stride as u32),
157 );
158 writer.add_metadata(
159 "moment.num_patches",
160 GGUFMetaValue::Uint32(config.num_patches as u32),
161 );
162 writer.add_metadata(
163 "moment.layer_norm_eps",
164 GGUFMetaValue::Float64(config.layer_norm_eps),
165 );
166}
167
168fn ggml_type_from_st(dtype: StDtype) -> anyhow::Result<GGMLType> {
169 match dtype {
170 StDtype::F32 => Ok(GGMLType::F32),
171 StDtype::F16 => Ok(GGMLType::F16),
172 StDtype::BF16 => Ok(GGMLType::BF16),
173 other => anyhow::bail!("unsupported safetensors dtype: {other:?}"),
174 }
175}
176
177fn cast_data(data: &[u8], src: GGMLType, dst: GGMLType) -> anyhow::Result<Vec<u8>> {
178 if src == dst {
179 return Ok(data.to_vec());
180 }
181 if dst == GGMLType::Q8_0 {
182 let f32_values = decode_to_f32(data, src)?;
183 return quantize_q8_0(&f32_values);
184 }
185 match (src, dst) {
186 (GGMLType::F32, GGMLType::F16) => {
187 let vals = parse_f32_le(data)?;
188 let mut out = Vec::with_capacity(vals.len() * 2);
189 for v in vals {
190 out.extend_from_slice(&f32_to_f16_bits(v).to_le_bytes());
191 }
192 Ok(out)
193 }
194 (GGMLType::F32, GGMLType::BF16) => {
195 let vals = parse_f32_le(data)?;
196 let mut out = Vec::with_capacity(vals.len() * 2);
197 for v in vals {
198 out.extend_from_slice(&((v.to_bits() >> 16) as u16).to_le_bytes());
199 }
200 Ok(out)
201 }
202 (GGMLType::F16, GGMLType::BF16) => {
203 let mut out = Vec::with_capacity(data.len());
204 for c in data.chunks_exact(2) {
205 let f32_val = f16_to_f32(u16::from_le_bytes([c[0], c[1]]));
206 out.extend_from_slice(&((f32_val.to_bits() >> 16) as u16).to_le_bytes());
207 }
208 Ok(out)
209 }
210 (GGMLType::BF16, GGMLType::F32) => {
211 let mut out = Vec::with_capacity(data.len() * 2);
212 for c in data.chunks_exact(2) {
213 let bits = (u16::from_le_bytes([c[0], c[1]]) as u32) << 16;
214 out.extend_from_slice(&bits.to_le_bytes());
215 }
216 Ok(out)
217 }
218 (GGMLType::BF16, GGMLType::F16) => {
219 let mut out = Vec::with_capacity(data.len());
220 for c in data.chunks_exact(2) {
221 let bits = (u16::from_le_bytes([c[0], c[1]]) as u32) << 16;
222 out.extend_from_slice(&f32_to_f16_bits(f32::from_bits(bits)).to_le_bytes());
223 }
224 Ok(out)
225 }
226 (GGMLType::F16, GGMLType::F32) => {
227 let mut out = Vec::with_capacity(data.len() * 2);
228 for c in data.chunks_exact(2) {
229 out.extend_from_slice(
230 &f16_to_f32(u16::from_le_bytes([c[0], c[1]]))
231 .to_bits()
232 .to_le_bytes(),
233 );
234 }
235 Ok(out)
236 }
237 _ => anyhow::bail!("unsupported cast: {src:?} → {dst:?}"),
238 }
239}
240
241fn decode_to_f32(data: &[u8], src: GGMLType) -> anyhow::Result<Vec<f32>> {
242 match src {
243 GGMLType::F32 => parse_f32_le(data),
244 GGMLType::F16 => data
245 .chunks_exact(2)
246 .map(|c| Ok(f16_to_f32(u16::from_le_bytes([c[0], c[1]]))))
247 .collect(),
248 GGMLType::BF16 => data
249 .chunks_exact(2)
250 .map(|c| {
251 Ok(f32::from_bits(
252 (u16::from_le_bytes([c[0], c[1]]) as u32) << 16,
253 ))
254 })
255 .collect(),
256 GGMLType::Q8_0 => anyhow::bail!("Q8_0 as source not supported"),
257 }
258}
259
260fn quantize_q8_0(values: &[f32]) -> anyhow::Result<Vec<u8>> {
261 const BLOCK: usize = 32;
262 if values.len() % BLOCK != 0 {
263 anyhow::bail!("Q8_0 requires count divisible by {BLOCK}");
264 }
265 let n_blocks = values.len() / BLOCK;
266 let mut out = vec![0u8; n_blocks * 34];
267 for b in 0..n_blocks {
268 let blk = &values[b * BLOCK..(b + 1) * BLOCK];
269 let amax = blk.iter().copied().map(f32::abs).fold(0.0f32, f32::max);
270 let d = if amax == 0.0 { 0.0f32 } else { amax / 127.0 };
271 let d_inv = if d == 0.0 { 0.0f32 } else { 1.0 / d };
272 let base = b * 34;
273 out[base..base + 2].copy_from_slice(&f32_to_f16_bits(d).to_le_bytes());
274 for i in 0..BLOCK {
275 out[base + 2 + i] = (blk[i] * d_inv).round().clamp(-127.0, 127.0) as i8 as u8;
276 }
277 }
278 Ok(out)
279}
280
281fn parse_f32_le(data: &[u8]) -> anyhow::Result<Vec<f32>> {
282 if data.len() % 4 != 0 {
283 anyhow::bail!("f32 data length not divisible by 4");
284 }
285 Ok(data
286 .chunks_exact(4)
287 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
288 .collect())
289}
290
291fn f32_to_f16_bits(v: f32) -> u16 {
292 let bits = v.to_bits();
293 let sign = ((bits >> 16) & 0x8000) as u16;
294 let exp = ((bits >> 23) & 0xFF) as i32;
295 let mantissa = bits & 0x007F_FFFF;
296 if exp == 0xFF {
297 return sign | 0x7C00 | if mantissa != 0 { 0x0200 } else { 0 };
298 }
299 let new_exp = exp - 127 + 15;
300 if new_exp >= 31 {
301 return sign | 0x7C00;
302 }
303 if new_exp <= 0 {
304 if new_exp < -10 {
305 return sign;
306 }
307 let m = (mantissa | 0x0080_0000) >> (1 - new_exp);
308 return sign | (m >> 13) as u16;
309 }
310 sign | ((new_exp as u16) << 10) | (mantissa >> 13) as u16
311}
312
313fn f16_to_f32(bits: u16) -> f32 {
314 let sign = ((bits & 0x8000) as u32) << 16;
315 let exp = ((bits >> 10) & 0x1F) as i32;
316 let mantissa = (bits & 0x03FF) as u32;
317 let f32_bits = if exp == 0 {
318 if mantissa == 0 {
319 sign
320 } else {
321 let mut m = mantissa;
322 let mut e = 0i32;
323 while m & 0x0400 == 0 {
324 m <<= 1;
325 e += 1;
326 }
327 sign | ((127 - 15 - e + 1) as u32) << 23 | (m & 0x03FF) << 13
328 }
329 } else if exp == 31 {
330 sign | 0x7F80_0000 | (mantissa << 13)
331 } else {
332 sign | ((exp + 127 - 15) as u32) << 23 | (mantissa << 13)
333 };
334 f32::from_bits(f32_bits)
335}