1mod rope;
11
12use std::io::{BufReader, Read, Seek};
13use std::path::{Path, PathBuf};
14
15use anyhow::{bail, Context, Result};
16use candle_core::quantized::gguf_file;
17use candle_core::{DType, Device, Tensor, D};
18
19use rope::RopeCache;
20use zsfm_nn::{linear, load_weight};
21
22use crate::config::Chronos2Config;
23
24#[derive(Clone, Debug)]
29pub struct InferConfig {
30 d_model: usize,
31 d_kv: usize,
32 d_ff: usize,
33 num_layers: usize,
34 num_heads: usize,
35 layer_norm_eps: f64,
36 rope_theta: f64,
37 patch_size: usize,
38 patch_stride: usize,
39 context_length: usize,
40 quantiles: Vec<f32>,
41 use_reg_token: bool,
42 use_arcsinh: bool,
43 time_encoding_scale: usize,
44 dense_act_fn: String,
45}
46
47impl From<&Chronos2Config> for InferConfig {
51 fn from(c2: &Chronos2Config) -> Self {
52 let cc = &c2.chronos_config;
53 InferConfig {
54 d_model: c2.d_model as usize,
55 d_kv: c2.d_kv as usize,
56 d_ff: c2.d_ff as usize,
57 num_layers: c2.num_layers as usize,
58 num_heads: c2.num_heads as usize,
59 layer_norm_eps: c2.layer_norm_epsilon,
60 rope_theta: c2.rope_theta,
61 patch_size: cc.input_patch_size as usize,
62 patch_stride: cc.input_patch_stride as usize,
63 context_length: cc.context_length as usize,
64 quantiles: cc.quantiles.clone(),
65 use_reg_token: cc.use_reg_token,
66 use_arcsinh: cc.use_arcsinh,
67 time_encoding_scale: c2.time_encoding_scale() as usize,
68 dense_act_fn: c2.dense_act_fn().to_string(),
69 }
70 }
71}
72
73impl InferConfig {
74 fn inner_dim(&self) -> usize { self.num_heads * self.d_kv }
75
76 pub fn patch_size(&self) -> usize { self.patch_size }
77 pub fn patch_stride(&self) -> usize { self.patch_stride }
78 pub fn context_length(&self) -> usize { self.context_length }
79 pub fn quantiles(&self) -> &[f32] { &self.quantiles }
80}
81
82pub struct ChronosModelBuilder {
99 gguf_path: PathBuf,
100 config: Option<InferConfig>,
101}
102
103impl ChronosModelBuilder {
104 fn new(gguf_path: impl Into<PathBuf>) -> Self {
105 Self { gguf_path: gguf_path.into(), config: None }
106 }
107
108 pub fn config(mut self, config: InferConfig) -> Self {
109 self.config = Some(config);
110 self
111 }
112
113 pub fn config_from(mut self, c2: &Chronos2Config) -> Self {
114 self.config = Some(InferConfig::from(c2));
115 self
116 }
117
118 pub fn build(self) -> Result<ChronosModel> {
119 let config = self
120 .config
121 .context("ChronosModelBuilder: no config set — call .config(...) or .config_from(...)")?;
122 ChronosModel::load(&self.gguf_path, config)
123 }
124}
125
126struct ResidualBlockWeights {
131 hidden_w: Tensor,
133 hidden_b: Tensor,
134 output_w: Tensor,
136 output_b: Tensor,
137 skip_w: Tensor,
139 skip_b: Tensor,
140}
141
142struct AttnWeights {
143 qkv_w: Tensor, o_w: Tensor,
145 norm_w: Tensor,
146}
147
148struct FfnWeights {
149 wi_w: Tensor,
150 wo_w: Tensor,
151 norm_w: Tensor,
152}
153
154struct BlockWeights {
155 time_attn: AttnWeights,
156 group_attn: AttnWeights,
157 ffn: FfnWeights,
158}
159
160pub struct ChronosModel {
165 device: Device,
166 pub config: InferConfig,
167 rope: RopeCache,
168 token_embd: Tensor,
170 input_patch: ResidualBlockWeights,
171 blocks: Vec<BlockWeights>,
172 enc_norm: Tensor,
173 output_patch: ResidualBlockWeights,
174}
175
176fn load_tensor(
181 content: &gguf_file::Content,
182 reader: &mut (impl Read + Seek),
183 name: &str,
184 device: &Device,
185) -> Result<Tensor> {
186 zsfm_nn::load_tensor(content, reader, name, device, DType::F32)
187}
188
189fn load_residual_block(
190 content: &gguf_file::Content,
191 reader: &mut (impl Read + Seek),
192 prefix: &str,
193 d_out_h: usize, d_out: usize, device: &Device,
196) -> Result<ResidualBlockWeights> {
197 let hidden_w = load_weight(content, reader, &format!("{prefix}.hidden.weight"), d_out_h, device)?;
199 let hidden_b = load_tensor(content, reader, &format!("{prefix}.hidden.bias"), device)?;
200 let output_w = load_weight(content, reader, &format!("{prefix}.output.weight"), d_out, device)?;
202 let output_b = load_tensor(content, reader, &format!("{prefix}.output.bias"), device)?;
203 let skip_w = load_weight(content, reader, &format!("{prefix}.skip.weight"), d_out, device)?;
205 let skip_b = load_tensor(content, reader, &format!("{prefix}.skip.bias"), device)?;
206 Ok(ResidualBlockWeights { hidden_w, hidden_b, output_w, output_b, skip_w, skip_b })
207}
208
209fn load_attn(
210 content: &gguf_file::Content,
211 reader: &mut (impl Read + Seek),
212 blk: usize,
213 kind: &str,
214 d_model: usize,
215 inner_dim: usize,
216 device: &Device,
217) -> Result<AttnWeights> {
218 let p = |s: &str| format!("blk.{blk}.{kind}.{s}");
219 let norm_name = format!("blk.{blk}.{kind}_norm.weight");
220 let q_w = load_weight(content, reader, &p("q.weight"), inner_dim, device)?;
221 let k_w = load_weight(content, reader, &p("k.weight"), inner_dim, device)?;
222 let v_w = load_weight(content, reader, &p("v.weight"), inner_dim, device)?;
223 let qkv_w = Tensor::cat(&[&q_w, &k_w, &v_w], 0)
224 .with_context(|| format!("qkv cat blk.{blk}.{kind}"))?;
225 Ok(AttnWeights {
226 qkv_w,
227 o_w: load_weight(content, reader, &p("o.weight"), d_model, device)?,
228 norm_w: load_tensor(content, reader, &norm_name, device)?,
229 })
230}
231
232impl ChronosModel {
233 pub fn builder(gguf_path: impl Into<PathBuf>) -> ChronosModelBuilder {
235 ChronosModelBuilder::new(gguf_path)
236 }
237
238 pub fn load(gguf_path: &Path, config: InferConfig) -> Result<Self> {
239 let device = Device::Cpu;
240 let file = std::fs::File::open(gguf_path)
241 .with_context(|| format!("open {}", gguf_path.display()))?;
242 let mut reader = BufReader::with_capacity(zsfm_gguf::READ_BUF_CAPACITY, file);
243 let content = gguf_file::Content::read(&mut reader).context("parse GGUF header")?;
244
245 let d = config.d_model;
246 let ff = config.d_ff;
247 let id = config.inner_dim();
248 let ps = config.patch_size;
249 let nq = config.quantiles.len();
250
251 let token_embd = load_tensor(&content, &mut reader, "token_embd.weight", &device)?;
252
253 let input_patch = load_residual_block(
255 &content, &mut reader, "input_patch", ff, d, &device,
256 )?;
257
258 let mut blocks = Vec::with_capacity(config.num_layers);
260 for n in 0..config.num_layers {
261 let time_attn = load_attn(&content, &mut reader, n, "time_attn", d, id, &device)?;
262 let group_attn = load_attn(&content, &mut reader, n, "group_attn", d, id, &device)?;
263 let ffn = FfnWeights {
264 wi_w: load_weight(&content, &mut reader, &format!("blk.{n}.ffn.wi.weight"), ff, &device)?,
265 wo_w: load_weight(&content, &mut reader, &format!("blk.{n}.ffn.wo.weight"), d, &device)?,
266 norm_w: load_tensor(&content, &mut reader, &format!("blk.{n}.ffn_norm.weight"), &device)?,
267 };
268 blocks.push(BlockWeights { time_attn, group_attn, ffn });
269 }
270
271 let enc_norm = load_tensor(&content, &mut reader, "enc_norm.weight", &device)?;
272
273 let out_d = nq * ps;
275 let output_patch = load_residual_block(
276 &content, &mut reader, "output_patch", ff, out_d, &device,
277 )?;
278
279 let rope = RopeCache::new(config.d_kv, 8192, config.rope_theta, &device)?;
280
281 Ok(Self { device, config, rope, token_embd, input_patch, blocks, enc_norm, output_patch })
282 }
283
284 pub fn forecast(&self, context: &[f32], prediction_length: usize) -> Result<Vec<Vec<f32>>> {
293 let cfg = &self.config;
294 let ps = cfg.patch_size;
295 let stride = cfg.patch_stride;
296 let n_quantiles = cfg.quantiles.len();
297
298 let n_out_patches = (prediction_length + ps - 1) / ps;
300
301 let (normalized, loc, scale) = instance_norm(context, cfg.use_arcsinh);
303
304 let padded = pad_for_patching(&normalized, ps);
306 let n_ctx_patches = (padded.len() - ps) / stride + 1;
307
308 let ctx_features = build_patch_features(
310 &padded, n_ctx_patches, ps, stride,
311 -((n_ctx_patches * ps) as f32), 0.0, cfg.time_encoding_scale as f32,
313 true, );
315
316 let fut_features = build_patch_features_future(
318 n_out_patches, ps,
319 0.0, cfg.time_encoding_scale as f32,
321 );
322
323 let ctx_tensor = Tensor::from_vec(ctx_features, (n_ctx_patches, 3 * ps), &self.device)?;
325 let fut_tensor = Tensor::from_vec(fut_features, (n_out_patches, 3 * ps), &self.device)?;
326
327 let ctx_embeds = self.forward_residual_block(&ctx_tensor, &self.input_patch)?;
328 let fut_embeds = self.forward_residual_block(&fut_tensor, &self.input_patch)?;
329
330 let seq = if cfg.use_reg_token {
332 let reg_embed = self.token_embd.narrow(0, 1, 1)?; Tensor::cat(&[&ctx_embeds, ®_embed, &fut_embeds], 0)?
335 } else {
336 Tensor::cat(&[&ctx_embeds, &fut_embeds], 0)?
337 };
338 let total_seq = seq.dim(0)?;
340
341 let hidden = seq.unsqueeze(0)?;
343
344 let hidden = self.forward_encoder(hidden, total_seq)?;
346 let forecast_embeds = hidden.narrow(1, total_seq - n_out_patches, n_out_patches)?;
350 let forecast_embeds = forecast_embeds.squeeze(0)?; let quantile_raw = self.forward_residual_block(&forecast_embeds, &self.output_patch)?;
355 let total_out = n_out_patches * ps;
359 let quantile_raw = quantile_raw.reshape((n_out_patches, n_quantiles, ps))?;
360 let quantile_raw = quantile_raw.permute([1, 0, 2])?.contiguous()?;
361 let quantile_raw = quantile_raw.reshape((n_quantiles, total_out))?;
362
363 let quantile_raw = if total_out > prediction_length {
365 quantile_raw.narrow(1, 0, prediction_length)?
366 } else {
367 quantile_raw
368 };
369
370 let data = quantile_raw.to_vec2::<f32>()?;
372 let mut result = vec![vec![0.0f32; prediction_length]; n_quantiles];
373 for q in 0..n_quantiles {
374 for t in 0..prediction_length {
375 let v = data[q][t] as f64;
376 let v = if cfg.use_arcsinh { v.sinh() } else { v };
377 result[q][t] = (v as f32) * scale + loc;
378 }
379 }
380 Ok(result)
381 }
382
383 fn forward_encoder(&self, mut x: Tensor, seq_len: usize) -> Result<Tensor> {
388 for blk in &self.blocks {
389 x = self.forward_block(x, blk, seq_len)?;
390 }
391 apply_t5_rms_norm(&x, &self.enc_norm, self.config.layer_norm_eps)
393 }
394
395 fn forward_block(
396 &self,
397 x: Tensor,
398 blk: &BlockWeights,
399 seq_len: usize,
400 ) -> Result<Tensor> {
401 let normed = apply_t5_rms_norm(&x, &blk.time_attn.norm_w, self.config.layer_norm_eps)?;
403 let attn_out = self.forward_time_attn(&normed, &blk.time_attn, seq_len)?;
404 let x = (&x + &attn_out)?;
405
406 let normed = apply_t5_rms_norm(&x, &blk.group_attn.norm_w, self.config.layer_norm_eps)?;
408 let grp_out = self.forward_group_attn_univariate(&normed, &blk.group_attn)?;
409 let x = (&x + &grp_out)?;
410
411 let normed = apply_t5_rms_norm(&x, &blk.ffn.norm_w, self.config.layer_norm_eps)?;
413 let ffn_out = self.forward_ffn(&normed, &blk.ffn)?;
414 Ok((&x + &ffn_out)?)
415 }
416
417 fn forward_time_attn(
422 &self,
423 x: &Tensor, w: &AttnWeights,
425 seq_len: usize,
426 ) -> Result<Tensor> {
427 let id = self.config.inner_dim();
428 let nh = self.config.num_heads;
429 let dkv = self.config.d_kv;
430
431 let qkv = linear(x, &w.qkv_w, None)?;
433 let q = qkv.narrow(D::Minus1, 0, id)?;
434 let k = qkv.narrow(D::Minus1, id, id)?;
435 let v = qkv.narrow(D::Minus1, 2 * id, id)?;
436
437 let q = q.reshape((1, seq_len, nh, dkv))?.permute([0, 2, 1, 3])?.contiguous()?;
439 let k = k.reshape((1, seq_len, nh, dkv))?.permute([0, 2, 1, 3])?.contiguous()?;
440 let v = v.reshape((1, seq_len, nh, dkv))?.permute([0, 2, 1, 3])?.contiguous()?;
441
442 let q = self.rope.apply(&q, seq_len)?;
443 let k = self.rope.apply(&k, seq_len)?;
444
445 let scores = q.matmul(&k.transpose(D::Minus1, D::Minus2)?)?;
447 let attn = candle_nn::ops::softmax_last_dim(&scores)?;
449
450 let out = attn.matmul(&v)?;
452
453 let out = out.permute([0, 2, 1, 3])?.contiguous()?.reshape((1, seq_len, id))?;
455
456 linear(&out, &w.o_w, None)
458 }
459
460 fn forward_group_attn_univariate(&self, x: &Tensor, w: &AttnWeights) -> Result<Tensor> {
470 let id = self.config.inner_dim();
474 let qkv = linear(x, &w.qkv_w, None)?;
475 let v = qkv.narrow(D::Minus1, 2 * id, id)?;
476 linear(&v, &w.o_w, None)
477 }
478
479 fn forward_ffn(&self, x: &Tensor, w: &FfnWeights) -> Result<Tensor> {
484 let h = linear(x, &w.wi_w, None)?;
485 let h = apply_act(&h, &self.config.dense_act_fn)?;
486 linear(&h, &w.wo_w, None)
487 }
488
489 fn forward_residual_block(&self, x: &Tensor, w: &ResidualBlockWeights) -> Result<Tensor> {
494 let h = linear(x, &w.hidden_w, Some(&w.hidden_b))?;
495 let h = apply_act(&h, &self.config.dense_act_fn)?;
496 let out = linear(&h, &w.output_w, Some(&w.output_b))?;
497 let skip = linear(x, &w.skip_w, Some(&w.skip_b))?;
498 Ok((&out + &skip)?)
499 }
500}
501
502fn apply_t5_rms_norm(x: &Tensor, weight: &Tensor, eps: f64) -> Result<Tensor> {
509 zsfm_nn::rms_norm(x, Some(weight), eps)
510}
511
512fn apply_act(x: &Tensor, name: &str) -> Result<Tensor> {
514 match name {
515 "relu" => Ok(x.relu()?),
516 "gelu" => Ok(x.gelu_erf()?), "gelu_new" | "gelu_pytorch_tanh" => Ok(x.gelu()?), "silu" | "swish" => Ok(candle_nn::ops::silu(x)?),
519 other => bail!("unsupported activation function: {other}"),
520 }
521}
522
523fn instance_norm(x: &[f32], use_arcsinh: bool) -> (Vec<f32>, f32, f32) {
530 let n = x.len() as f64;
532 let loc = x.iter().map(|&v| v as f64).sum::<f64>() / n;
533 let var = x.iter().map(|&v| (v as f64 - loc).powi(2)).sum::<f64>() / n;
534 let scale = (var.sqrt() as f32).max(1e-5);
535
536 let mut out: Vec<f32> = x.iter().map(|&v| (v as f64 - loc) as f32 / scale).collect();
537 if use_arcsinh {
538 for v in &mut out {
539 *v = (*v as f64).asinh() as f32;
540 }
541 }
542 (out, loc as f32, scale)
543}
544
545fn pad_for_patching(x: &[f32], patch_size: usize) -> Vec<f32> {
551 let rem = x.len() % patch_size;
552 if rem == 0 {
553 x.to_vec()
554 } else {
555 let pad_len = patch_size - rem;
556 let mut out = vec![f32::NAN; pad_len];
557 out.extend_from_slice(x);
558 out
559 }
560}
561
562fn build_patch_features(
569 padded: &[f32],
570 n_patches: usize,
571 patch_size: usize,
572 stride: usize,
573 time_start: f32,
574 _time_end: f32,
575 time_scale: f32,
576 observed: bool,
577) -> Vec<f32> {
578 let feature_dim = 3 * patch_size;
579 let mut out = vec![0.0f32; n_patches * feature_dim];
580
581 for p in 0..n_patches {
582 let offset = p * stride;
583 let base = p * feature_dim;
584
585 for i in 0..patch_size {
586 let t = offset + i;
587 let global_t = time_start + t as f32;
588
589 out[base + i] = global_t / time_scale;
591
592 let v = padded.get(t).copied().unwrap_or(0.0);
594 let is_obs = observed && v.is_finite();
595 out[base + patch_size + i] = if is_obs { v } else { 0.0 };
596
597 out[base + 2 * patch_size + i] = if is_obs { 1.0 } else { 0.0 };
599 }
600 }
601 out
602}
603
604fn build_patch_features_future(
606 n_patches: usize,
607 patch_size: usize,
608 time_start: f32,
609 time_scale: f32,
610) -> Vec<f32> {
611 let feature_dim = 3 * patch_size;
612 let mut out = vec![0.0f32; n_patches * feature_dim];
613
614 for p in 0..n_patches {
615 let offset = p * patch_size;
616 let base = p * feature_dim;
617 for i in 0..patch_size {
618 let global_t = time_start + (offset + i) as f32;
619 out[base + i] = global_t / time_scale;
620 }
622 }
623 out
624}
625
626impl zsfm_core::Forecaster for ChronosModel {
631 type Config = InferConfig;
632
633 fn load(gguf_path: &Path, config: InferConfig) -> Result<Self> {
634 ChronosModel::load(gguf_path, config)
635 }
636
637 fn forecast(
640 &self,
641 context: &[Vec<f32>],
642 _mask: &[Vec<bool>],
643 horizon: usize,
644 ) -> Result<zsfm_core::QuantileMatrix> {
645 anyhow::ensure!(context.len() == 1, "ChronosModel only supports univariate forecasting (1 variate)");
646 let qmat = ChronosModel::forecast(self, &context[0], horizon)?; Ok(qmat.into_iter().map(|row| vec![row]).collect())
648 }
649}