1use std::path::PathBuf;
2
3use crate::common::DtypeArg;
4use anyhow::Context;
5use clap::{Subcommand, ValueEnum};
6use serde::Serialize;
7
8use zsfm_gguf::GGMLType;
9use zsfm_mitra::config::MitraConfig;
10use zsfm_mitra::convert::{convert, ConvertOptions};
11
12#[derive(Clone, ValueEnum)]
13pub enum TaskArg {
14 Classification,
15 Regression,
16}
17
18#[derive(Subcommand)]
19pub enum Command {
20 Convert {
28 #[arg(short, long, default_value = "autogluon/mitra-classifier")]
31 model: String,
32 #[arg(long, value_enum, default_value = "classification")]
33 task: TaskArg,
34 #[arg(short, long)]
35 output: Option<PathBuf>,
36 #[arg(long, default_value = "f32")]
37 dtype: DtypeArg,
38 #[arg(long, default_value = "models")]
39 model_dir: PathBuf,
40 #[arg(long, env = "HF_TOKEN")]
41 token: Option<String>,
42 #[arg(long)]
44 redownload: bool,
45 },
46 InspectTensors { path: PathBuf },
49 Upload {
51 #[arg(short, long, default_value = "amaye15/mitra-gguf")]
52 repo: String,
53 #[arg(long, env = "HF_TOKEN")]
54 token: String,
55 #[arg(long, default_value = ".")]
56 root: PathBuf,
57 },
58 Infer {
71 #[arg(short, long)]
72 gguf: PathBuf,
73 #[arg(long, value_enum, default_value = "classification")]
74 task: TaskArg,
75 },
76
77 Delete {
84 #[arg(short, long, default_value = "autogluon/mitra-classifier")]
86 model: String,
87 #[arg(long, value_enum, default_value = "classification")]
89 task: TaskArg,
90 #[arg(long, default_value = "models")]
92 model_dir: PathBuf,
93 #[arg(short, long)]
95 output: Option<PathBuf>,
96 },
97}
98
99fn dtype_name(d: &DtypeArg) -> &'static str {
100 match d {
101 DtypeArg::F32 => "f32",
102 DtypeArg::F16 => "f16",
103 DtypeArg::Q8 => "q8",
104 }
105}
106
107pub async fn run(command: Command) -> anyhow::Result<()> {
108 match command {
109 Command::Delete {
110 model,
111 task,
112 model_dir,
113 output,
114 } => {
115 let task_str = match task {
116 TaskArg::Classification => "classification",
117 TaskArg::Regression => "regression",
118 };
119 let variant_dir = model_dir.join(format!("mitra-{task_str}"));
120 let canonical = zsfm_hub::canonical_gguf_path(&variant_dir, &model);
121 crate::common::delete_cached_model(&canonical, output.as_deref())?;
122 }
123 Command::Upload { repo, token, root } => {
124 zsfm_hub::upload_repo(&repo, &token, &root).await?;
125 }
126
127 Command::Convert {
128 model,
129 task,
130 output,
131 dtype,
132 model_dir,
133 token,
134 redownload,
135 } => {
136 let task_str = match task {
137 TaskArg::Classification => "classification",
138 TaskArg::Regression => "regression",
139 };
140 let variant_dir = model_dir.join(format!("mitra-{task_str}"));
141 let output = output.unwrap_or_else(|| {
142 PathBuf::from(format!("gguf/mitra-{task_str}-{}.gguf", dtype_name(&dtype)))
143 });
144 let canonical = zsfm_hub::canonical_gguf_path(&variant_dir, &model);
145
146 if crate::common::try_recast_from_cache(&canonical, &output, dtype.into(), redownload)?
147 {
148 return Ok(());
149 }
150
151 eprintln!("Downloading {model} into {} …", variant_dir.display());
152 let files = zsfm_hub::download_model(&model, token.as_deref(), &variant_dir)
153 .await
154 .context("download failed")?;
155 let safetensors_path = files
156 .safetensors_shards
157 .first()
158 .context("no safetensors file downloaded")?;
159
160 let config = match task {
161 TaskArg::Classification => MitraConfig::classifier(),
162 TaskArg::Regression => MitraConfig::regressor(),
163 };
164 if files.config_json.exists() {
165 let config_str =
166 std::fs::read_to_string(&files.config_json).context("read config.json")?;
167 let v: serde_json::Value =
168 serde_json::from_str(&config_str).context("parse config.json")?;
169 println!(
170 "HF config.json: dim={} n_layers={} n_heads={} dim_output={}",
171 v.get("dim").and_then(|x| x.as_u64()).unwrap_or(0),
172 v.get("n_layers").and_then(|x| x.as_u64()).unwrap_or(0),
173 v.get("n_heads").and_then(|x| x.as_u64()).unwrap_or(0),
174 v.get("dim_output").and_then(|x| x.as_u64()).unwrap_or(0),
175 );
176 }
177
178 let f32_opts = ConvertOptions {
179 output_dtype: GGMLType::F32,
180 };
181 convert(
182 std::slice::from_ref(safetensors_path),
183 &config,
184 &f32_opts,
185 &canonical,
186 )?;
187 eprintln!("Wrote canonical F32 GGUF to {} …", canonical.display());
188 files.cleanup_weights();
189
190 zsfm_checkpoint::recast(&canonical, &output, dtype.into())?;
191 eprintln!("Wrote {}", output.display());
192 }
193
194 Command::InspectTensors { path } => crate::common::inspect_tensors(&path)?,
195
196 Command::Infer { gguf, task } => {
197 run_infer_burn(&gguf, &task).await?;
198 }
199 }
200
201 Ok(())
202}
203
204async fn run_infer_burn(gguf: &std::path::PathBuf, task: &TaskArg) -> anyhow::Result<()> {
205 use zsfm_mitra::infer::BurnMitraModel;
206
207 let buf = zsfm_core::read_stdin_limited()?;
208 let req: serde_json::Value = serde_json::from_str(&buf).context("parse JSON input")?;
209
210 let x_support = zsfm_core::parse_matrix(&req["x_support"])?;
211 let x_query = zsfm_core::parse_matrix(&req["x_query"])?;
212
213 let config = match task {
214 TaskArg::Classification => MitraConfig::classifier(),
215 TaskArg::Regression => MitraConfig::regressor(),
216 };
217
218 eprintln!("Loading Burn model from {} …", gguf.display());
219 let model = BurnMitraModel::load(gguf, config).context("load model")?;
220
221 match task {
222 TaskArg::Classification => {
223 let y_support: Vec<usize> = serde_json::from_value(req["y_support"].clone())
224 .context("expected a 1D JSON integer array for `y_support`")?;
225 let n_classes = req
226 .get("n_classes")
227 .and_then(|v| v.as_u64())
228 .unwrap_or_else(|| {
229 y_support
230 .iter()
231 .copied()
232 .max()
233 .map(|m| m as u64 + 1)
234 .unwrap_or(1)
235 }) as usize;
236
237 eprintln!(
238 "Running classification ({} support / {} query rows, {n_classes} classes) …",
239 x_support.len(),
240 x_query.len()
241 );
242 let logits = model
243 .predict_classification(&x_support, &y_support, &x_query, n_classes)
244 .context("predict")?;
245 let probabilities: Vec<Vec<f32>> =
246 logits.iter().map(|row| zsfm_core::softmax(row)).collect();
247
248 #[derive(Serialize)]
249 struct Resp<'a> {
250 task: &'static str,
251 logits: &'a [Vec<f32>],
252 probabilities: Vec<Vec<f32>>,
253 }
254 println!(
255 "{}",
256 serde_json::to_string_pretty(&Resp {
257 task: "classification",
258 logits: &logits,
259 probabilities
260 })?
261 );
262 }
263 TaskArg::Regression => {
264 let y_support: Vec<f32> = serde_json::from_value(req["y_support"].clone())
265 .context("expected a 1D JSON numeric array for `y_support`")?;
266 eprintln!(
267 "Running regression ({} support / {} query rows) …",
268 x_support.len(),
269 x_query.len()
270 );
271 let predictions = model
272 .predict_regression(&x_support, &y_support, &x_query)
273 .context("predict")?;
274 #[derive(Serialize)]
275 struct Resp {
276 task: &'static str,
277 predictions: Vec<f32>,
278 }
279 println!(
280 "{}",
281 serde_json::to_string_pretty(&Resp {
282 task: "regression",
283 predictions
284 })?
285 );
286 }
287 }
288 Ok(())
289}