1use std::path::PathBuf;
2
3use crate::common::DtypeArg;
4use anyhow::Context;
5use clap::Subcommand;
6
7use zsfm_core::{ForecastOutput, VariateForecast};
8use zsfm_gguf::GGMLType;
9use zsfm_moirai2::config::Moirai2Config;
10use zsfm_moirai2::convert::{convert, ConvertOptions};
11
12#[derive(Subcommand)]
13pub enum Command {
14 Upload {
16 #[arg(short, long, default_value = "amaye15/moirai2-gguf")]
17 repo: String,
18 #[arg(long, env = "HF_TOKEN")]
19 token: String,
20 #[arg(long, default_value = ".")]
22 root: PathBuf,
23 },
24 Convert {
32 #[arg(short, long, default_value = "Salesforce/moirai-2.0-R-small")]
33 model: String,
34 #[arg(short, long, default_value = "gguf/moirai2-f32.gguf")]
35 output: 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 Infer {
65 #[arg(short, long, default_value = "gguf/moirai2-f32.gguf")]
66 gguf: PathBuf,
67 },
68
69 Delete {
76 #[arg(short, long, default_value = "Salesforce/moirai-2.0-R-small")]
78 model: String,
79 #[arg(long, default_value = "models")]
81 model_dir: PathBuf,
82 #[arg(short, long)]
84 output: Option<PathBuf>,
85 },
86}
87
88pub async fn run(command: Command) -> anyhow::Result<()> {
89 match command {
90 Command::Delete {
91 model,
92 model_dir,
93 output,
94 } => {
95 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, &model);
96 crate::common::delete_cached_model(&canonical, output.as_deref())?;
97 }
98 Command::Upload { repo, token, root } => {
99 zsfm_hub::upload_repo(&repo, &token, &root).await?;
100 }
101
102 Command::Convert {
103 model,
104 output,
105 dtype,
106 model_dir,
107 token,
108 redownload,
109 } => {
110 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, &model);
111 if crate::common::try_recast_from_cache(&canonical, &output, dtype.into(), redownload)?
112 {
113 return Ok(());
114 }
115
116 eprintln!("Downloading {model} into {} …", model_dir.display());
117 let files = zsfm_hub::download_model(&model, token.as_deref(), &model_dir)
118 .await
119 .context("download failed")?;
120
121 let config = Moirai2Config::default();
122 let f32_opts = ConvertOptions {
123 output_dtype: GGMLType::F32,
124 };
125 convert(&files.safetensors_shards, &config, &f32_opts, &canonical)?;
126 eprintln!("Wrote canonical F32 GGUF to {} …", canonical.display());
127 files.cleanup_weights();
128
129 zsfm_checkpoint::recast(&canonical, &output, dtype.into())?;
130 eprintln!("Wrote {}", output.display());
131 }
132
133 Command::InspectTensors { path } => crate::common::inspect_tensors(&path)?,
134
135 Command::Infer { gguf } => {
136 run_infer_burn(&gguf).await?;
137 }
138 }
139
140 Ok(())
141}
142
143async fn run_infer_burn(gguf: &std::path::PathBuf) -> anyhow::Result<()> {
144 use zsfm_moirai2::infer::BurnMoirai2Model;
145
146 let buf = zsfm_core::read_stdin_limited()?;
147 let req: serde_json::Value = serde_json::from_str(&buf).context("parse JSON input")?;
148 let contexts = zsfm_core::parse_mv_contexts(req["context"].clone())?;
149 let horizon = zsfm_core::parse_horizon(&req)?;
150 let config = Moirai2Config::default();
151
152 eprintln!("Loading Burn model from {} …", gguf.display());
153 let model = BurnMoirai2Model::load(gguf, config).context("load model")?;
154
155 let mut fc_outputs: Vec<ForecastOutput> = Vec::new();
156 let mut total_ctx: usize = 0;
157
158 for raw_variates in &contexts {
159 let n_var = raw_variates.len();
160 anyhow::ensure!(n_var > 0, "each context must have at least one variate");
161
162 if n_var == 1 {
163 let ctx = &raw_variates[0];
164 anyhow::ensure!(!ctx.is_empty(), "context series must not be empty");
165 total_ctx += ctx.len();
166 let point = model.forecast(ctx, horizon).context("forecast")?;
167 fc_outputs.push(ForecastOutput::Univariate {
168 point,
169 quantiles: Default::default(),
170 });
171 } else {
172 let mut var_forecasts: Vec<VariateForecast> = Vec::with_capacity(n_var);
173 for (vi, ctx) in raw_variates.iter().enumerate() {
174 anyhow::ensure!(!ctx.is_empty(), "variate {vi} context must not be empty");
175 total_ctx += ctx.len();
176 let point = model
177 .forecast(ctx, horizon)
178 .with_context(|| format!("forecast variate {vi}"))?;
179 var_forecasts.push(VariateForecast {
180 point,
181 quantiles: Default::default(),
182 });
183 }
184 fc_outputs.push(ForecastOutput::Multivariate {
185 variates: var_forecasts,
186 });
187 }
188 }
189 println!(
190 "{}",
191 zsfm_core::forecast_response_json("moirai2", total_ctx, horizon, fc_outputs)?
192 );
193 Ok(())
194}