1mod rope;
16
17use std::collections::HashMap;
18use std::sync::Mutex;
19use std::io::BufReader;
20use std::path::{Path, PathBuf};
21
22use anyhow::{bail, Context, Result};
23use candle_core::quantized::gguf_file;
24use candle_core::{DType, Device, Tensor, D};
25use candle_nn::ops;
26
27use rope::RopeCache;
28use zsfm_core::{json_bool, json_f64, json_usize};
29use zsfm_nn::{load_tensor, try_load_tensor};
30
31const SILU_SCALE: f64 = 1.766782948312328;
32
33#[derive(Clone, Debug)]
38pub struct InferConfig {
39 d_model: usize,
40 num_layers: usize,
41 num_heads: usize,
42 num_groups: usize,
43 qk_dim: usize,
44 v_dim: usize,
45 patch_size: usize,
46 norm_eps: f64,
47 layer_group_size: usize,
48 num_variate_layers_per_group: usize,
49 variate_layer_first: bool,
50 use_xpos: bool,
51 residual_mult: f64,
52 residual_attn_ratio: f64,
53 compute_f64: bool,
56}
57
58impl Default for InferConfig {
59 fn default() -> Self {
61 InferConfig {
62 d_model: 2048,
63 num_layers: 48,
64 num_heads: 32,
65 num_groups: 32,
66 qk_dim: 64,
67 v_dim: 64,
68 patch_size: 32,
69 norm_eps: 5e-4,
70 layer_group_size: 48,
71 num_variate_layers_per_group: 1,
72 variate_layer_first: false,
73 use_xpos: true,
74 residual_mult: 0.75,
75 residual_attn_ratio: 5.136215466577748,
76 compute_f64: false,
77 }
78 }
79}
80
81impl InferConfig {
82 fn is_variate_layer(&self, idx: usize) -> bool {
83 if self.variate_layer_first {
84 idx % self.layer_group_size < self.num_variate_layers_per_group
85 } else {
86 idx % self.layer_group_size
87 >= self.layer_group_size - self.num_variate_layers_per_group
88 }
89 }
90 fn q_size(&self) -> usize { self.qk_dim * self.num_heads }
91 fn k_size(&self) -> usize { self.qk_dim * self.num_groups }
92 fn v_size(&self) -> usize { self.v_dim * self.num_groups }
93
94 pub fn with_d_model(mut self, v: usize) -> Self { self.d_model = v; self }
97 pub fn with_num_layers(mut self, v: usize) -> Self { self.num_layers = v; self }
98 pub fn with_num_heads(mut self, v: usize) -> Self { self.num_heads = v; self }
99 pub fn with_num_groups(mut self, v: usize) -> Self { self.num_groups = v; self }
100 pub fn with_qk_dim(mut self, v: usize) -> Self { self.qk_dim = v; self }
101 pub fn with_v_dim(mut self, v: usize) -> Self { self.v_dim = v; self }
102 pub fn with_patch_size(mut self, v: usize) -> Self { self.patch_size = v; self }
103 pub fn with_norm_eps(mut self, v: f64) -> Self { self.norm_eps = v; self }
104 pub fn with_layer_group_size(mut self, v: usize) -> Self { self.layer_group_size = v; self }
105 pub fn with_num_variate_layers_per_group(mut self, v: usize) -> Self {
106 self.num_variate_layers_per_group = v;
107 self
108 }
109 pub fn with_variate_layer_first(mut self, v: bool) -> Self { self.variate_layer_first = v; self }
110 pub fn with_use_xpos(mut self, v: bool) -> Self { self.use_xpos = v; self }
111 pub fn with_residual_mult(mut self, v: f64) -> Self { self.residual_mult = v; self }
112 pub fn with_residual_attn_ratio(mut self, v: f64) -> Self { self.residual_attn_ratio = v; self }
113
114 pub fn with_compute_f64(mut self, v: bool) -> Self { self.compute_f64 = v; self }
117
118 pub fn patch_size(&self) -> usize { self.patch_size }
121 pub fn compute_f64(&self) -> bool { self.compute_f64 }
122
123 pub fn from_json_value(v: &serde_json::Value) -> Self {
129 Self::default()
130 .with_d_model(json_usize(v, "d_model", 2048))
131 .with_num_layers(json_usize(v, "num_layers", 48))
132 .with_num_heads(json_usize(v, "num_heads", 32))
133 .with_num_groups(json_usize(v, "num_groups", 32))
134 .with_qk_dim(json_usize(v, "qk_dim", 64))
135 .with_v_dim(json_usize(v, "v_dim", 64))
136 .with_patch_size(json_usize(v, "patch_size", 32))
137 .with_norm_eps(json_f64(v, "norm_eps", 5e-4))
138 .with_layer_group_size(json_usize(v, "layer_group_size", 48))
139 .with_num_variate_layers_per_group(json_usize(v, "num_variate_layers_per_group", 1))
140 .with_variate_layer_first(json_bool(v, "variate_layer_first", false))
141 .with_use_xpos(json_bool(v, "use_xpos", true))
142 .with_residual_mult(json_f64(v, "residual_mult", 0.75))
143 .with_residual_attn_ratio(json_f64(v, "residual_attn_ratio", 5.136215466577748))
144 }
145}
146
147pub struct TotoModelBuilder {
166 gguf_path: PathBuf,
167 config: InferConfig,
168}
169
170impl TotoModelBuilder {
171 fn new(gguf_path: impl Into<PathBuf>) -> Self {
172 Self { gguf_path: gguf_path.into(), config: InferConfig::default() }
173 }
174
175 pub fn config(mut self, config: InferConfig) -> Self {
178 self.config = config;
179 self
180 }
181
182 pub fn config_json(mut self, v: &serde_json::Value) -> Self {
184 self.config = InferConfig::from_json_value(v);
185 self
186 }
187
188 pub fn with_compute_f64(mut self, v: bool) -> Self {
191 self.config = self.config.with_compute_f64(v);
192 self
193 }
194
195 pub fn build(self) -> Result<TotoModel> {
196 TotoModel::load(&self.gguf_path, self.config)
197 }
198}
199
200struct ResidualMlpWeights {
205 l1_w: Tensor, l1_b: Tensor,
206 l2_w: Tensor, l2_b: Tensor,
207 skip_w: Tensor, skip_b: Tensor,
208 tau: f64,
209 is_output: bool, }
211
212struct BlockWeights {
213 attn_qkv_w: Tensor,
214 attn_qkv_b: Option<Tensor>,
215 attn_out_w: Tensor,
216 attn_out_b: Option<Tensor>,
217 attn_pds: Option<Tensor>, attn_tau: f64,
219 ffn_up_w: Tensor,
220 ffn_down_w: Tensor,
221 mlp_tau: f64,
222}
223
224pub struct TotoModel {
229 device: Device,
230 pub config: InferConfig,
231 rope: RopeCache,
232 patch_proj: ResidualMlpWeights,
233 blocks: Vec<BlockWeights>,
234 output_head: ResidualMlpWeights,
235 causal_mask_cache: Mutex<HashMap<usize, Tensor>>,
236}
237
238fn compute_taus(num_layers: usize, residual_mult: f64, residual_attn_ratio: f64) -> (Vec<f64>, Vec<f64>) {
247 let total_depth = 2 * num_layers;
248 let alpha_mlp = residual_mult * (2.0 / (1.0 + residual_attn_ratio.powi(2))).sqrt();
249 let alpha_attn = residual_attn_ratio * alpha_mlp;
250
251 let tau = |index: usize| -> f64 {
252 let n_attn = (index + 1) / 2;
253 let n_mlp = index / 2;
254 let num = if index % 2 == 0 { alpha_attn } else { alpha_mlp };
255 let den = (total_depth as f64 / 2.0
256 + n_attn as f64 * alpha_attn.powi(2)
257 + n_mlp as f64 * alpha_mlp.powi(2))
258 .sqrt();
259 num / den
260 };
261
262 (0..num_layers).map(|i| (tau(2 * i), tau(2 * i + 1))).unzip()
263}
264
265impl TotoModel {
266 pub fn builder(gguf_path: impl Into<PathBuf>) -> TotoModelBuilder {
268 TotoModelBuilder::new(gguf_path)
269 }
270
271 pub fn load(gguf_path: &Path, config: InferConfig) -> Result<Self> {
272 let device = Device::Cpu;
273 let dtype = if config.compute_f64 { DType::F64 } else { DType::F32 };
274 let file = std::fs::File::open(gguf_path)
275 .with_context(|| format!("open {}", gguf_path.display()))?;
276 let mut reader = BufReader::with_capacity(zsfm_gguf::READ_BUF_CAPACITY, file);
277 let content = gguf_file::Content::read(&mut reader)
278 .context("parse GGUF header")?;
279
280 macro_rules! ld {
281 ($name:expr) => { load_tensor(&content, &mut reader, $name, &device, dtype) };
282 }
283 macro_rules! tld {
284 ($name:expr) => { try_load_tensor(&content, &mut reader, $name, &device, dtype) };
285 }
286
287 let patch_proj = ResidualMlpWeights {
289 l1_w: ld!("patch_proj.linear1.weight")?,
290 l1_b: ld!("patch_proj.linear1.bias")?,
291 l2_w: ld!("patch_proj.linear2.weight")?,
292 l2_b: ld!("patch_proj.linear2.bias")?,
293 skip_w: ld!("patch_proj.skip_proj.weight")?,
294 skip_b: ld!("patch_proj.skip_proj.bias")?,
295 tau: 1.0,
296 is_output: false,
297 };
298
299 let (attn_taus, mlp_taus) = compute_taus(
302 config.num_layers,
303 config.residual_mult,
304 config.residual_attn_ratio,
305 );
306
307 let mut blocks = Vec::with_capacity(config.num_layers);
309 for n in 0..config.num_layers {
310 blocks.push(BlockWeights {
311 attn_qkv_w: ld!(&format!("blk.{n}.attn_qkv.weight"))?,
312 attn_qkv_b: tld!(&format!("blk.{n}.attn_qkv.bias"))?,
313 attn_out_w: ld!(&format!("blk.{n}.attn_output.weight"))?,
314 attn_out_b: tld!(&format!("blk.{n}.attn_output.bias"))?,
315 attn_pds: tld!(&format!("blk.{n}.attn_pds.weight"))?,
316 attn_tau: attn_taus[n],
317 ffn_up_w: ld!(&format!("blk.{n}.ffn_up.weight"))?,
318 ffn_down_w: {
319 let w = ld!(&format!("blk.{n}.ffn_down.weight"))?;
320 if w.dim(0)? != config.d_model {
323 w.t()?.contiguous()?
324 } else {
325 w
326 }
327 },
328 mlp_tau: mlp_taus[n],
329 });
330 }
331
332 let output_head = ResidualMlpWeights {
334 l1_w: ld!("output_head.linear1.weight")?,
335 l1_b: ld!("output_head.linear1.bias")?,
336 l2_w: ld!("output_head.linear2.weight")?,
337 l2_b: ld!("output_head.linear2.bias")?,
338 skip_w: ld!("output_head.skip_proj.weight")?,
339 skip_b: ld!("output_head.skip_proj.bias")?,
340 tau: 1.0,
341 is_output: true,
342 };
343
344 let rope = RopeCache::new(config.qk_dim, 8192);
345 Ok(Self { device, config, rope, patch_proj, blocks, output_head, causal_mask_cache: Mutex::new(HashMap::new()) })
346 }
347
348 pub fn forecast(
362 &self,
363 target: &[Vec<f32>],
364 mask: &[Vec<bool>],
365 prediction_length: usize,
366 ) -> Result<Vec<Vec<Vec<f32>>>> {
367 let n_var = target.len();
368 let ctx_len = target[0].len();
369 let patch_size = self.config.patch_size;
370
371 if ctx_len % patch_size != 0 {
372 bail!("ctx_len ({ctx_len}) must be divisible by patch_size ({patch_size})");
373 }
374 let ctx_patches = ctx_len / patch_size;
375 let fcst_patches = (prediction_length + patch_size - 1) / patch_size;
377 let total_patches = ctx_patches + fcst_patches;
378
379 let mut locs = Vec::with_capacity(n_var);
381 let mut scales = Vec::with_capacity(n_var);
382 for v in 0..n_var {
383 let (loc, scale) = causal_patched_std_scaler(&target[v], &mask[v], patch_size);
384 locs.push(loc);
385 scales.push(scale);
386 }
387
388 let mut patch_data = vec![0.0f32; n_var * total_patches * 2 * patch_size];
391 for v in 0..n_var {
392 for p in 0..ctx_patches {
394 let base = v * total_patches * 2 * patch_size + p * 2 * patch_size;
395 for i in 0..patch_size {
396 let t = p * patch_size + i;
397 let obs = mask[v][t];
398 let val = if obs {
399 (((target[v][t] - locs[v][t]) / scales[v][t]) as f64).asinh() as f32
400 } else {
401 0.0
402 };
403 patch_data[base + i] = val;
404 patch_data[base + patch_size + i] = if obs { 0.0 } else { 1.0 };
405 }
406 }
407 for p in ctx_patches..total_patches {
409 let base = v * total_patches * 2 * patch_size + p * 2 * patch_size;
410 for i in 0..patch_size {
411 patch_data[base + i] = 0.0;
412 patch_data[base + patch_size + i] = 1.0; }
414 }
415 }
416
417 let dtype = if self.config.compute_f64 { DType::F64 } else { DType::F32 };
418 let x = Tensor::from_vec(
419 patch_data,
420 (1usize, n_var, total_patches, 2 * patch_size),
421 &self.device,
422 )?.to_dtype(dtype)?;
423
424 let x = self.forward_residual_mlp(&x, &self.patch_proj)?;
426
427 let x = self.forward_transformer(x, n_var, total_patches)?;
429
430 let x = self.forward_residual_mlp(&x, &self.output_head)?;
432 let x = x.narrow(2, ctx_patches - 1, fcst_patches)?;
438 let fcst_steps = fcst_patches * patch_size;
440
441 let out = x.reshape((1usize, n_var, fcst_patches, patch_size, 9))?;
443 let out = out.permute([4, 0, 1, 2, 3])?.contiguous()?;
445 let out = out.reshape((9usize, n_var, fcst_steps))?;
447
448 let out = if fcst_steps > prediction_length {
450 out.narrow(2, 0, prediction_length)?
451 } else {
452 out
453 };
454 let out_data = out.to_dtype(DType::F32)?.to_vec3::<f32>()?; let loc_final: Vec<f32> = (0..n_var).map(|v| locs[v][ctx_len - 1]).collect();
459 let scale_final: Vec<f32> = (0..n_var).map(|v| scales[v][ctx_len - 1]).collect();
460
461 let mut result = vec![vec![vec![0.0f32; prediction_length]; n_var]; 9];
462 for q in 0..9 {
463 for v in 0..n_var {
464 for t in 0..prediction_length {
465 let raw = out_data[q][v][t] as f64;
466 result[q][v][t] = (raw.sinh() as f32) * scale_final[v] + loc_final[v];
467 }
468 }
469 }
470 Ok(result)
471 }
472
473 fn forward_residual_mlp(&self, x: &Tensor, w: &ResidualMlpWeights) -> Result<Tensor> {
478 let h = uu_linear(x, &w.l1_w, Some(&w.l1_b))?;
480 let h = uu_silu(&h)?;
481
482 let h = if w.is_output {
483 uu_linear_readout(&h, &w.l2_w, Some(&w.l2_b))?
484 } else {
485 uu_linear(&h, &w.l2_w, Some(&w.l2_b))?
486 };
487
488 let skip = if w.is_output {
489 uu_linear_readout(x, &w.skip_w, Some(&w.skip_b))?
490 } else {
491 uu_linear(x, &w.skip_w, Some(&w.skip_b))?
492 };
493
494 residual_add(&h, &skip, w.tau)
495 }
496
497 fn forward_transformer(&self, mut x: Tensor, n_var: usize, num_patches: usize) -> Result<Tensor> {
502 for (idx, blk) in self.blocks.iter().enumerate() {
503 x = if self.config.is_variate_layer(idx) {
504 self.forward_variate_layer(x, blk, n_var, num_patches)?
505 } else {
506 self.forward_time_layer(x, blk, n_var, num_patches)?
507 };
508 }
509 rms_norm(&x, self.config.norm_eps)
510 }
511
512 fn forward_time_layer(
513 &self,
514 x: Tensor,
515 blk: &BlockWeights,
516 n_var: usize,
517 num_patches: usize,
518 ) -> Result<Tensor> {
519 let cfg = &self.config;
520 let state = x.reshape((n_var, num_patches, cfg.d_model))?;
522
523 let normed = rms_norm(&state, cfg.norm_eps)?;
524 let seq_ids: Vec<u32> = (0..num_patches as u32).collect();
525 let attn_out = self.forward_attention(&normed, blk, &seq_ids, false)?;
526 let state = residual_add(&attn_out, &state, blk.attn_tau)?;
527
528 let normed = rms_norm(&state, cfg.norm_eps)?;
529 let ffn_out = self.forward_ffn(&normed, blk)?;
530 let state = residual_add(&ffn_out, &state, blk.mlp_tau)?;
531
532 Ok(state.reshape((1usize, n_var, num_patches, cfg.d_model))?)
534 }
535
536 fn forward_variate_layer(
537 &self,
538 x: Tensor,
539 blk: &BlockWeights,
540 n_var: usize,
541 num_patches: usize,
542 ) -> Result<Tensor> {
543 let cfg = &self.config;
544 let state = x.permute([0, 2, 1, 3])?.contiguous()?.reshape((num_patches, n_var, cfg.d_model))?;
546
547 let normed = rms_norm(&state, cfg.norm_eps)?;
548 let attn_out = self.forward_attention(&normed, blk, &[], true)?;
549 let state = residual_add(&attn_out, &state, blk.attn_tau)?;
550
551 let normed = rms_norm(&state, cfg.norm_eps)?;
552 let ffn_out = self.forward_ffn(&normed, blk)?;
553 let state = residual_add(&ffn_out, &state, blk.mlp_tau)?;
554
555 Ok(state
557 .reshape((1usize, num_patches, n_var, cfg.d_model))?
558 .permute([0, 2, 1, 3])?
559 .contiguous()?)
560 }
561
562 fn forward_attention(
567 &self,
568 state: &Tensor,
569 blk: &BlockWeights,
570 seq_ids: &[u32],
571 is_variate: bool,
572 ) -> Result<Tensor> {
573 let cfg = &self.config;
574 let (batch, seq, _d) = state.dims3()?;
575
576 let qkv = uu_linear(state, &blk.attn_qkv_w, blk.attn_qkv_b.as_ref())?;
578 let q = qkv.narrow(D::Minus1, 0, cfg.q_size())?.contiguous()?;
579 let k = qkv.narrow(D::Minus1, cfg.q_size(), cfg.k_size())?.contiguous()?;
580 let v = qkv.narrow(D::Minus1, cfg.q_size() + cfg.k_size(), cfg.v_size())?.contiguous()?;
581
582 let q = q.reshape((batch, seq, cfg.num_heads, cfg.qk_dim))?.permute([0, 2, 1, 3])?.contiguous()?;
584 let k = k.reshape((batch, seq, cfg.num_groups, cfg.qk_dim))?.permute([0, 2, 1, 3])?.contiguous()?;
585 let v = v.reshape((batch, seq, cfg.num_groups, cfg.v_dim))?.permute([0, 2, 1, 3])?.contiguous()?;
586
587 let q = match blk.attn_pds.as_ref() {
589 Some(pds_w) => apply_per_dim_scale(&q, pds_w)?,
590 None => q,
591 };
592
593 let (q, k) = if !is_variate && !seq_ids.is_empty() && cfg.use_xpos {
595 let q = self.rope.apply(&q, seq_ids, 1.0, &self.device)?;
596 let k = self.rope.apply(&k, seq_ids, -1.0, &self.device)?;
597 (q, k)
598 } else {
599 (q, k)
600 };
601
602 let scale = 1.0 / cfg.qk_dim as f64;
604 let scores = (q.matmul(&k.transpose(D::Minus1, D::Minus2)?)? * scale)?;
605
606 let scores = if !is_variate {
608 self.apply_causal_mask(scores, seq)?
609 } else {
610 scores
611 };
612
613 let attn = ops::softmax_last_dim(&scores)?;
614 let out = attn.matmul(&v)?;
615 let out = out.permute([0, 2, 1, 3])?.contiguous()?.reshape((batch, seq, cfg.num_heads * cfg.v_dim))?;
617
618 uu_linear(&out, &blk.attn_out_w, blk.attn_out_b.as_ref())
619 }
620
621 fn forward_ffn(&self, x: &Tensor, blk: &BlockWeights) -> Result<Tensor> {
626 let fc1_out = uu_linear(x, &blk.ffn_up_w, None)?;
627 let half = fc1_out.dim(D::Minus1)? / 2;
628 let gate = fc1_out.narrow(D::Minus1, 0, half)?;
629 let val = fc1_out.narrow(D::Minus1, half, half)?;
630 let activated = (gate * ops::silu(&val)?)?;
632 uu_linear(&activated, &blk.ffn_down_w, None)
633 }
634}
635
636fn uu_linear(x: &Tensor, w: &Tensor, b: Option<&Tensor>) -> Result<Tensor> {
645 linear_with_scale(x, w, b, 1.0 / (w.dim(1)? as f64).sqrt())
646}
647
648fn uu_linear_readout(x: &Tensor, w: &Tensor, b: Option<&Tensor>) -> Result<Tensor> {
649 linear_with_scale(x, w, b, 1.0 / w.dim(1)? as f64)
650}
651
652fn linear_with_scale(x: &Tensor, w: &Tensor, b: Option<&Tensor>, scale: f64) -> Result<Tensor> {
653 let shape = x.dims().to_vec();
654 let d_in = *shape.last().unwrap();
655 let batch: usize = shape[..shape.len() - 1].iter().product();
656 let d_out = w.dim(0)?;
657
658 let x_flat = x.reshape((batch, d_in))?;
659 let out_flat = x_flat.matmul(&w.t()?)?; let mut out_shape = shape[..shape.len() - 1].to_vec();
662 out_shape.push(d_out);
663 let out = out_flat.reshape(out_shape)?;
664 let out = if let Some(b) = b { out.broadcast_add(b)? } else { out };
665 Ok((out * scale)?)
666}
667
668fn uu_silu(x: &Tensor) -> Result<Tensor> {
669 Ok((ops::silu(x)? * SILU_SCALE)?)
670}
671
672fn rms_norm(x: &Tensor, eps: f64) -> Result<Tensor> {
673 zsfm_nn::rms_norm(x, None, eps)
674}
675
676fn residual_add(h: &Tensor, skip: &Tensor, tau: f64) -> Result<Tensor> {
677 let denom = (1.0 + tau * tau).sqrt();
678 Ok(((h * (tau / denom))? + (skip * (1.0 / denom))?)?)
679}
680
681fn apply_per_dim_scale(q: &Tensor, pds_w: &Tensor) -> Result<Tensor> {
682 let sp = (pds_w.exp()? + 1.0)?.log()?; let log2 = std::f64::consts::LN_2;
686 let r = (sp / log2)?;
687 Ok(q.broadcast_mul(&r)?)
688}
689
690impl TotoModel {
691 fn apply_causal_mask(&self, scores: Tensor, seq: usize) -> Result<Tensor> {
692 let mut cache = self.causal_mask_cache.lock().unwrap();
693 if !cache.contains_key(&seq) {
694 let mut mask_data = vec![0.0f32; seq * seq];
695 for i in 0..seq {
696 for j in (i + 1)..seq {
697 mask_data[i * seq + j] = f32::NEG_INFINITY;
698 }
699 }
700 let dtype = if self.config.compute_f64 { DType::F64 } else { DType::F32 };
701 let mask = Tensor::from_vec(mask_data, (seq, seq), &self.device)?.to_dtype(dtype)?;
702 cache.insert(seq, mask);
703 }
704 Ok(scores.broadcast_add(&cache[&seq])?)
705 }
706}
707
708fn causal_patched_std_scaler(
713 data: &[f32],
714 mask: &[bool],
715 patch_size: usize,
716) -> (Vec<f32>, Vec<f32>) {
717 let n = data.len();
718 let num_patches = n / patch_size;
719
720 let mut cum_count = 0.0f64;
721 let mut m1 = 0.0f64;
722 let mut m2 = 0.0f64;
723 let correction = 1.0f64;
724 let minimum_scale = 1e-6f64;
725
726 let mut patch_loc = vec![0.0f32; num_patches];
727 let mut patch_scale = vec![1e-6f32; num_patches];
728
729 for p in 0..num_patches {
730 for i in 0..patch_size {
731 let t = p * patch_size + i;
732 if mask[t] {
733 let x = data[t] as f64;
734 cum_count += 1.0;
735 let prev_m1 = m1;
736 m1 += (x - m1) / cum_count;
737 m2 += (x - prev_m1) * (x - m1);
738 }
739 }
740 patch_loc[p] = m1 as f32;
741 let denom = (cum_count - correction).max(1.0);
742 patch_scale[p] = (m2 / denom).sqrt().max(minimum_scale) as f32;
743 }
744
745 let mut loc = vec![0.0f32; n];
746 let mut scale = vec![1e-6f32; n];
747 for p in 0..num_patches {
748 for i in 0..patch_size {
749 let t = p * patch_size + i;
750 loc[t] = patch_loc[p];
751 scale[t] = patch_scale[p];
752 }
753 }
754 (loc, scale)
755}
756
757impl zsfm_core::Forecaster for TotoModel {
762 type Config = InferConfig;
763
764 fn load(gguf_path: &Path, config: InferConfig) -> Result<Self> {
765 TotoModel::load(gguf_path, config)
766 }
767
768 fn forecast(
769 &self,
770 context: &[Vec<f32>],
771 mask: &[Vec<bool>],
772 horizon: usize,
773 ) -> Result<zsfm_core::QuantileMatrix> {
774 TotoModel::forecast(self, context, mask, horizon)
775 }
776}