1use std::fs::File;
14use std::io::BufWriter;
15use std::path::PathBuf;
16
17use anyhow::Context;
18use clap::{Args, ValueEnum};
19
20use zsfm_checkpoint::{cast, load_checkpoint, LoadOptions};
21use zsfm_gguf::{GGMLType, GGUFMetaValue, GGUFWriter};
22
23#[derive(Clone, ValueEnum)]
24pub enum DtypeArg {
25 F32,
26 F16,
27 Bf16,
28 Q8,
29}
30
31impl From<DtypeArg> for GGMLType {
32 fn from(d: DtypeArg) -> Self {
33 match d {
34 DtypeArg::F32 => GGMLType::F32,
35 DtypeArg::F16 => GGMLType::F16,
36 DtypeArg::Bf16 => GGMLType::BF16,
37 DtypeArg::Q8 => GGMLType::Q8_0,
38 }
39 }
40}
41
42#[derive(Args)]
43pub struct ConvertArgs {
44 #[arg(required_unless_present = "repo")]
50 pub inputs: Vec<PathBuf>,
51 #[arg(long, conflicts_with = "inputs", value_name = "OWNER/NAME")]
54 pub repo: Option<String>,
55 #[arg(long, requires = "repo")]
59 pub format: Option<String>,
60 #[arg(long, requires = "repo")]
64 pub file: Option<String>,
65 #[arg(long, default_value = "main", requires = "repo")]
67 pub revision: String,
68 #[arg(long, env = "HF_TOKEN", requires = "repo")]
70 pub token: Option<String>,
71 #[arg(long, requires = "repo")]
73 pub model_dir: Option<PathBuf>,
74 #[arg(short, long)]
76 pub output: PathBuf,
77 #[arg(long, default_value = "f16")]
79 pub dtype: DtypeArg,
80 #[arg(long)]
82 pub arch: Option<String>,
83 #[arg(long)]
85 pub name: Option<String>,
86 #[arg(long = "metadata", value_name = "KEY=VALUE")]
89 pub metadata: Vec<String>,
90 #[arg(long)]
92 pub pickle_key: Option<String>,
93 #[arg(long)]
95 pub strip_prefix: Option<String>,
96}
97
98#[derive(Args)]
99pub struct PullArgs {
100 #[arg(value_name = "OWNER/NAME")]
102 pub repo: String,
103 #[arg(long)]
107 pub format: Option<String>,
108 #[arg(long)]
111 pub file: Option<String>,
112 #[arg(long, default_value = "main")]
114 pub revision: String,
115 #[arg(long, env = "HF_TOKEN")]
117 pub token: Option<String>,
118 #[arg(long)]
120 pub model_dir: Option<PathBuf>,
121}
122
123#[derive(Args)]
124pub struct InspectArgs {
125 pub path: PathBuf,
127 #[arg(long, value_name = "N")]
130 pub values: Option<usize>,
131 #[arg(long)]
133 pub tensor: Option<String>,
134 #[arg(long)]
136 pub pickle_key: Option<String>,
137}
138
139pub async fn run_convert(args: ConvertArgs) -> anyhow::Result<()> {
140 let (inputs, repo_default_name) = if let Some(repo) = &args.repo {
143 let model_dir = args
144 .model_dir
145 .clone()
146 .unwrap_or_else(|| PathBuf::from("models").join(repo.replace('/', "__")));
147 println!("Looking up files in {repo}@{} …", args.revision);
148 let downloaded = zsfm_hub::download_any_format(
149 repo,
150 args.format.as_deref(),
151 args.file.as_deref(),
152 &args.revision,
153 args.token.as_deref(),
154 &model_dir,
155 )
156 .await
157 .with_context(|| format!("download {repo}"))?;
158 println!(
159 "Downloaded {} {} file(s) into {} …",
160 downloaded.checkpoint_files.len(),
161 downloaded.format,
162 model_dir.display()
163 );
164 let default_name = repo.rsplit('/').next().unwrap_or(repo).to_string();
165 (downloaded.checkpoint_files, Some(default_name))
166 } else {
167 (args.inputs.clone(), None)
168 };
169
170 let opts = LoadOptions {
171 pickle_key: args.pickle_key.clone(),
172 strip_prefix: args.strip_prefix.clone(),
173 };
174 println!("Loading {} input file(s) …", inputs.len());
175 let ckpt = load_checkpoint(&inputs, &opts)?;
176 println!("Loaded {} tensors.", ckpt.tensors.len());
177 anyhow::ensure!(!ckpt.tensors.is_empty(), "checkpoint contains no tensors");
178
179 let mut writer = GGUFWriter::new();
180
181 let mut meta: Vec<(String, GGUFMetaValue)> = ckpt.metadata;
185 let default_name = repo_default_name.unwrap_or_else(|| {
186 inputs[0]
187 .file_stem()
188 .and_then(|s| s.to_str())
189 .unwrap_or("model")
190 .to_string()
191 });
192 let set = |meta: &mut Vec<(String, GGUFMetaValue)>, key: &str, value: GGUFMetaValue| {
193 meta.retain(|(k, _)| k != key);
194 meta.push((key.to_string(), value));
195 };
196 if args.arch.is_some() || !meta.iter().any(|(k, _)| k == "general.architecture") {
197 let arch = args.arch.clone().unwrap_or_else(|| "unknown".to_string());
198 set(
199 &mut meta,
200 "general.architecture",
201 GGUFMetaValue::String(arch),
202 );
203 }
204 if args.name.is_some() || !meta.iter().any(|(k, _)| k == "general.name") {
205 let name = args.name.clone().unwrap_or(default_name);
206 set(&mut meta, "general.name", GGUFMetaValue::String(name));
207 }
208 for pair in &args.metadata {
209 let (key, value) = pair
210 .split_once('=')
211 .with_context(|| format!("--metadata {pair}: expected key=value"))?;
212 set(&mut meta, key, parse_meta_value(value));
213 }
214 for (key, value) in meta {
215 writer.add_metadata(key, value);
216 }
217
218 let out_dtype: GGMLType = args.dtype.into();
219 let mut fallback_count = 0usize;
220 for t in &ckpt.tensors {
221 let (data, dst, fell_back) =
222 cast::cast_with_q8_fallback(&t.data, t.dtype, &t.shape, out_dtype)
223 .with_context(|| format!("tensor {}: cast failed", t.name))?;
224 fallback_count += fell_back as usize;
225 let gguf_shape: Vec<u64> = t.shape.iter().rev().copied().collect();
226 writer.add_tensor(t.name.clone(), gguf_shape, dst, data);
227 }
228 if fallback_count > 0 {
229 eprintln!("note: {fallback_count} tensor(s) fell back to F32 (too small for Q8_0 blocks)");
230 }
231
232 if let Some(parent) = args.output.parent() {
233 if !parent.as_os_str().is_empty() {
234 std::fs::create_dir_all(parent)?;
235 }
236 }
237 println!(
238 "Writing {} tensors to {} …",
239 ckpt.tensors.len(),
240 args.output.display()
241 );
242 let out_file =
243 File::create(&args.output).with_context(|| format!("create {}", args.output.display()))?;
244 writer.write_to(&mut BufWriter::new(out_file))?;
245 println!("Done.");
246 Ok(())
247}
248
249pub async fn run_pull(args: PullArgs) -> anyhow::Result<()> {
253 let model_dir = args
254 .model_dir
255 .clone()
256 .unwrap_or_else(|| PathBuf::from("models").join(args.repo.replace('/', "__")));
257 println!("Looking up files in {}@{} …", args.repo, args.revision);
258 let downloaded = zsfm_hub::download_any_format(
259 &args.repo,
260 args.format.as_deref(),
261 args.file.as_deref(),
262 &args.revision,
263 args.token.as_deref(),
264 &model_dir,
265 )
266 .await
267 .with_context(|| format!("download {}", args.repo))?;
268
269 println!(
270 "Downloaded {} file(s) in {} format:",
271 downloaded.checkpoint_files.len(),
272 downloaded.format
273 );
274 for f in &downloaded.checkpoint_files {
275 println!(" {}", f.display());
276 }
277 if let Some(cfg) = &downloaded.config_json {
278 println!(" {} (config.json)", cfg.display());
279 }
280 println!(
281 "\nTo convert: zsfm convert {} -o model.gguf --dtype f16",
282 downloaded
283 .checkpoint_files
284 .iter()
285 .map(|p| p.display().to_string())
286 .collect::<Vec<_>>()
287 .join(" ")
288 );
289 Ok(())
290}
291
292fn parse_meta_value(s: &str) -> GGUFMetaValue {
293 match s {
294 "true" => return GGUFMetaValue::Bool(true),
295 "false" => return GGUFMetaValue::Bool(false),
296 _ => {}
297 }
298 if let Ok(v) = s.parse::<u32>() {
299 return GGUFMetaValue::Uint32(v);
300 }
301 if let Ok(v) = s.parse::<i32>() {
302 return GGUFMetaValue::Int32(v);
303 }
304 if let Ok(v) = s.parse::<f32>() {
305 return GGUFMetaValue::Float32(v);
306 }
307 GGUFMetaValue::String(s.to_string())
308}
309
310pub fn run_inspect(args: InspectArgs) -> anyhow::Result<()> {
311 let ext = args
312 .path
313 .extension()
314 .and_then(|e| e.to_str())
315 .unwrap_or_default()
316 .to_ascii_lowercase();
317
318 if ext == "gguf" {
320 let mut file = std::fs::File::open(&args.path)
321 .with_context(|| format!("open {}", args.path.display()))?;
322 {
325 use std::io::Seek;
326 if let Ok(gguf) = zsfm_gguf::GGUFFile::read(&mut file) {
327 println!("Metadata in {}:", args.path.display());
328 for (key, value) in &gguf.metadata {
329 println!(" {key:48} {value:?}");
330 }
331 println!("Tensors:");
332 let mut infos: Vec<&zsfm_gguf::GGUFTensorInfo> = gguf.tensors.iter().collect();
333 infos.sort_by(|a, b| a.name.cmp(&b.name));
334 for info in &infos {
335 let py_shape: Vec<u64> = info.shape.iter().rev().copied().collect();
336 println!(" {:64} {:?} {py_shape:?}", info.name, info.dtype);
337 }
338 if let Some(n) = args.values {
339 for info in infos {
340 if let Some(only) = &args.tensor {
341 if info.name != *only {
342 continue;
343 }
344 }
345 let vals = gguf.tensor_f32(&mut file, info)?;
346 let shown: Vec<String> =
347 vals.iter().take(n).map(|v| v.to_string()).collect();
348 println!(
349 " {} values[..{}]: [{}]",
350 info.name,
351 shown.len(),
352 shown.join(", ")
353 );
354 }
355 }
356 return Ok(());
357 }
358 file.seek(std::io::SeekFrom::Start(0))?;
359 }
360 anyhow::bail!(
361 "cannot inspect {}: GGUF uses a quantization format outside F32/F16/BF16/Q8_0",
362 args.path.display()
363 );
364 }
365
366 let opts = LoadOptions {
367 pickle_key: args.pickle_key.clone(),
368 strip_prefix: None,
369 };
370 let ckpt = load_checkpoint(&[args.path.clone()], &opts)?;
371 println!("Tensors in {}:", args.path.display());
372 let mut tensors: Vec<_> = ckpt.tensors.iter().collect();
373 tensors.sort_by(|a, b| a.name.cmp(&b.name));
374 for t in &tensors {
375 println!(" {:64} {} {:?}", t.name, t.dtype.name(), t.shape);
376 }
377 if let Some(n) = args.values {
378 for t in tensors {
379 if let Some(only) = &args.tensor {
380 if &t.name != only {
381 continue;
382 }
383 }
384 let vals = cast::decode_to_f32(&t.data, t.dtype)?;
385 let shown: Vec<String> = vals.iter().take(n).map(|v| v.to_string()).collect();
386 println!(
387 " {} values[..{}]: [{}]",
388 t.name,
389 shown.len(),
390 shown.join(", ")
391 );
392 }
393 }
394 Ok(())
395}