1use std::collections::HashMap;
2use std::io::BufReader;
3use std::sync::Mutex;
4use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result};
7use candle_core::{DType, Device, Tensor};
8use candle_core::quantized::gguf_file;
9use candle_nn::ops;
10use simdeez::prelude::*;
11
12use crate::config::FlowStateConfig;
13
14simd_runtime_generate!(
15 fn ssm_scan_step(
16 state_r: &mut [f32], state_i: &mut [f32],
17 a_r: &[f32], a_i: &[f32],
18 bu_r: &[f32], bu_i: &[f32],
19 ) {
20 let mut sr = &mut state_r[..]; let mut si = &mut state_i[..];
21 let mut ar = &a_r[..]; let mut ai = &a_i[..];
22 let mut br = &bu_r[..]; let mut bi = &bu_i[..];
23
24 while sr.len() >= S::Vf32::WIDTH {
25 let sr_v = S::Vf32::load_from_slice(sr);
26 let si_v = S::Vf32::load_from_slice(si);
27 let ar_v = S::Vf32::load_from_slice(ar);
28 let ai_v = S::Vf32::load_from_slice(ai);
29 let br_v = S::Vf32::load_from_slice(br);
30 let bi_v = S::Vf32::load_from_slice(bi);
31
32 let new_r = ar_v.mul_add(sr_v, ai_v.neg_mul_add(si_v, br_v));
34 let new_i = ar_v.mul_add(si_v, ai_v.mul_add(sr_v, bi_v));
36
37 new_r.copy_to_slice(sr);
38 new_i.copy_to_slice(si);
39
40 sr = &mut sr[S::Vf32::WIDTH..]; si = &mut si[S::Vf32::WIDTH..];
41 ar = &ar[S::Vf32::WIDTH..]; ai = &ai[S::Vf32::WIDTH..];
42 br = &br[S::Vf32::WIDTH..]; bi = &bi[S::Vf32::WIDTH..];
43 }
44
45 for j in 0..sr.len() {
46 let nr = ar[j] * sr[j] - ai[j] * si[j] + br[j];
47 let ni = ar[j] * si[j] + ai[j] * sr[j] + bi[j];
48 sr[j] = nr;
49 si[j] = ni;
50 }
51 }
52);
53
54#[derive(Debug, Clone)]
59pub struct InferConfig {
60 pub num_layers: usize,
61 pub embed_dim: usize,
62 pub state_dim: usize,
63 pub n_inputs: usize, pub decoder_dim: usize,
65 pub decoder_patch_len: usize,
66 pub quantiles: Vec<f32>,
67 pub basis_range: [f32; 2], pub context_length: usize,
69 pub eps: f32,
70}
71
72impl From<&FlowStateConfig> for InferConfig {
76 fn from(c: &FlowStateConfig) -> Self {
77 InferConfig {
78 num_layers: c.encoder_num_layers as usize,
79 embed_dim: c.embedding_feature_dim as usize,
80 state_dim: c.encoder_state_dim as usize,
81 n_inputs: c.n_inputs() as usize,
82 decoder_dim: c.decoder_dim as usize,
83 decoder_patch_len: c.decoder_patch_len as usize,
84 quantiles: c.quantiles.clone(),
85 basis_range: c.basis_range(),
86 context_length: c.context_length as usize,
87 eps: 1e-5,
88 }
89 }
90}
91
92impl InferConfig {
93 pub fn quantiles(&self) -> &[f32] { &self.quantiles }
94 pub fn median_index(&self) -> usize {
95 self.quantiles
96 .iter()
97 .position(|&q| (q - 0.5).abs() < 1e-6)
98 .unwrap_or(self.quantiles.len() / 2)
99 }
100}
101
102pub struct FlowStateModelBuilder {
119 gguf_path: PathBuf,
120 config: Option<InferConfig>,
121}
122
123impl FlowStateModelBuilder {
124 fn new(gguf_path: impl Into<PathBuf>) -> Self {
125 Self { gguf_path: gguf_path.into(), config: None }
126 }
127
128 pub fn config(mut self, config: InferConfig) -> Self {
129 self.config = Some(config);
130 self
131 }
132
133 pub fn config_from(mut self, c: &FlowStateConfig) -> Self {
134 self.config = Some(InferConfig::from(c));
135 self
136 }
137
138 pub fn build(self) -> Result<FlowStateModel> {
139 let config = self
140 .config
141 .context("FlowStateModelBuilder: no config set — call .config(...) or .config_from(...)")?;
142 FlowStateModel::load(&self.gguf_path, config)
143 }
144}
145
146struct S5Weights {
151 log_lambda_real: Vec<f32>, lambda_imag: Vec<f32>, b_r: Tensor, b_i: Tensor, c_r: Tensor, c_i: Tensor, d: Tensor, log_delta: Vec<f32>, }
160
161struct BlockWeights {
162 ssm: S5Weights,
163 out_weight: Tensor, out_bias: Tensor, norm_weight: Tensor, norm_bias: Tensor, disc_cache: Mutex<HashMap<u32, (Vec<f32>, Vec<f32>, Tensor, Tensor)>>,
169}
170
171pub struct FlowStateModel {
172 device: Device,
173 pub config: InferConfig,
174 embed_w: Tensor, embed_b: Tensor, blocks: Vec<BlockWeights>,
177 decoder_w: Tensor, decoder_b: Tensor, legendre_cache: Mutex<HashMap<usize, Tensor>>,
180}
181
182fn load_f32_vec(
187 content: &gguf_file::Content,
188 reader: &mut (impl std::io::Read + std::io::Seek),
189 name: &str,
190 device: &Device,
191) -> anyhow::Result<Vec<f32>> {
192 zsfm_nn::load_vec(content, reader, name, device)
193}
194
195fn load_matrix(
196 content: &gguf_file::Content,
197 reader: &mut (impl std::io::Read + std::io::Seek),
198 name: &str,
199 device: &Device,
200) -> anyhow::Result<Tensor> {
201 zsfm_nn::load_tensor(content, reader, name, device, DType::F32)
202}
203
204impl FlowStateModel {
205 pub fn load(gguf_path: &Path, config: InferConfig) -> anyhow::Result<Self> {
206 let device = Device::Cpu;
207 let file = std::fs::File::open(gguf_path)
208 .with_context(|| format!("open {}", gguf_path.display()))?;
209 let mut file = BufReader::with_capacity(zsfm_gguf::READ_BUF_CAPACITY, file);
210 let content = gguf_file::Content::read(&mut file).context("read GGUF header")?;
211
212 let embed_w = load_matrix(&content, &mut file, "embed.weight", &device)?;
213 let embed_b = load_matrix(&content, &mut file, "embed.bias", &device)?;
214
215 let mut blocks = Vec::with_capacity(config.num_layers);
216 for n in 0..config.num_layers {
217 let ssm = S5Weights {
218 log_lambda_real: load_f32_vec(&content, &mut file, &format!("blk.{n}.ssm.log_lambda_real"), &device)?,
219 lambda_imag: load_f32_vec(&content, &mut file, &format!("blk.{n}.ssm.lambda_imag"), &device)?,
220 b_r: load_matrix(&content, &mut file, &format!("blk.{n}.ssm.b_r"), &device)?,
221 b_i: load_matrix(&content, &mut file, &format!("blk.{n}.ssm.b_i"), &device)?,
222 c_r: load_matrix(&content, &mut file, &format!("blk.{n}.ssm.c_r"), &device)?,
223 c_i: load_matrix(&content, &mut file, &format!("blk.{n}.ssm.c_i"), &device)?,
224 d: load_matrix(&content, &mut file, &format!("blk.{n}.ssm.d"), &device)?,
225 log_delta: load_f32_vec(&content, &mut file, &format!("blk.{n}.ssm.log_delta"), &device)?,
226 };
227 blocks.push(BlockWeights {
228 ssm,
229 out_weight: load_matrix(&content, &mut file, &format!("blk.{n}.out.weight"), &device)?,
230 out_bias: load_matrix(&content, &mut file, &format!("blk.{n}.out.bias"), &device)?,
231 norm_weight: load_matrix(&content, &mut file, &format!("blk.{n}.norm.weight"), &device)?,
232 norm_bias: load_matrix(&content, &mut file, &format!("blk.{n}.norm.bias"), &device)?,
233 disc_cache: Mutex::new(HashMap::new()),
234 });
235 }
236
237 let decoder_w = load_matrix(&content, &mut file, "decoder.weight", &device)?;
238 let decoder_b = load_matrix(&content, &mut file, "decoder.bias", &device)?;
239
240 Ok(Self {
241 device,
242 config,
243 embed_w,
244 embed_b,
245 blocks,
246 decoder_w,
247 decoder_b,
248 legendre_cache: Mutex::new(HashMap::new()),
249 })
250 }
251
252 pub fn builder(gguf_path: impl Into<PathBuf>) -> FlowStateModelBuilder {
254 FlowStateModelBuilder::new(gguf_path)
255 }
256
257 pub fn forecast(&self, context: &[f32], prediction_length: usize) -> anyhow::Result<Vec<Vec<f32>>> {
264 let cfg = &self.config;
265
266 let ctx_len = context.len().min(cfg.context_length);
268 let start = context.len().saturating_sub(ctx_len);
269 let context = &context[start..];
270 let seq_len = context.len();
271
272 let (normed_values, final_mean, final_std) = causal_revin_norm(context, cfg.eps);
274 if std::env::var("FLOWSTATE_DEBUG").is_ok() {
275 eprintln!("RevIN final_mean={:.8} final_std={:.8}", final_mean, final_std);
276 eprintln!("normed[0..4]: {:.6} {:.6} {:.6} {:.6}",
277 normed_values[0], normed_values[1], normed_values[2], normed_values[3]);
278 eprintln!("normed[252..256]: {:.6} {:.6} {:.6} {:.6}",
279 normed_values[252], normed_values[253], normed_values[254], normed_values[255]);
280 }
281
282 let mut input_data = vec![0.0f32; seq_len * cfg.n_inputs];
284 for t in 0..seq_len {
285 input_data[t * cfg.n_inputs] = normed_values[t];
286 if cfg.n_inputs > 1 {
287 input_data[t * cfg.n_inputs + 1] = 0.0; }
289 }
290
291 let input_t = Tensor::from_vec(input_data, (seq_len, cfg.n_inputs), &self.device)?;
293 let mut hidden = linear(&input_t, &self.embed_w, &self.embed_b)?;
294
295 let scale_factor = cfg.decoder_patch_len as f32 / prediction_length as f32;
297
298 let scratch_len = seq_len * cfg.state_dim;
303 let mut scan_r = vec![0.0f32; scratch_len];
304 let mut scan_i = vec![0.0f32; scratch_len];
305 let mut state_r = vec![0.0f32; cfg.state_dim];
306 let mut state_i = vec![0.0f32; cfg.state_dim];
307 let num_layers = self.blocks.len();
308 for (i, block) in self.blocks.iter().enumerate() {
309 let is_last = i == num_layers - 1;
310 hidden = self.apply_s5_layer(
311 hidden, block, scale_factor, is_last,
312 &mut scan_r, &mut scan_i, &mut state_r, &mut state_i,
313 )?;
314 }
315 let n_q = cfg.quantiles.len();
319 let coeffs = linear(&hidden, &self.decoder_w, &self.decoder_b)?
320 .reshape((n_q, cfg.decoder_dim))?;
321
322 if std::env::var("FLOWSTATE_DEBUG").is_ok() {
323 let coeffs_data: Vec<f32> = coeffs.flatten_all()?.to_vec1()?;
324 for qi in 0..n_q {
325 for d in 0..cfg.decoder_dim {
326 eprintln!("COEFF,{qi},{d},{:.8}", coeffs_data[qi * cfg.decoder_dim + d]);
327 }
328 }
329 }
330
331 let basis = {
333 let mut cache = self.legendre_cache.lock().unwrap();
334 if !cache.contains_key(&prediction_length) {
335 let raw = legendre_basis(prediction_length, cfg.decoder_dim, cfg.basis_range,
336 scale_factor, cfg.decoder_patch_len);
337 let flat: Vec<f32> = raw.into_iter().flatten().collect();
338 cache.insert(prediction_length,
339 Tensor::from_vec(flat, (prediction_length, cfg.decoder_dim), &self.device)?);
340 }
341 cache[&prediction_length].clone()
342 };
343
344 let out_t = coeffs.matmul(&basis.t()?)?;
346
347 let out_raw: Vec<f32> = out_t.flatten_all()?.to_vec1()?;
349 let mut denormed = vec![vec![0.0f32; prediction_length]; n_q];
350 for q in 0..n_q {
351 for p in 0..prediction_length {
352 denormed[q][p] = out_raw[q * prediction_length + p] * final_std + final_mean;
353 }
354 }
355
356 let mut output = vec![vec![0.0f32; prediction_length]; n_q];
365 for p in 0..prediction_length {
366 let mut sorted: Vec<f32> = (0..n_q).map(|q| denormed[q][p]).collect();
367 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
368 for (qi, &prob) in cfg.quantiles.iter().enumerate() {
369 let idx = (n_q - 1) as f32 * prob;
370 let lower = idx.floor() as usize;
371 let upper = idx.ceil() as usize;
372 let weight = idx - lower as f32;
373 output[qi][p] = sorted[lower] * (1.0 - weight) + sorted[upper] * weight;
374 }
375 }
376
377 Ok(output)
378 }
379
380 #[allow(clippy::too_many_arguments)]
388 fn apply_s5_layer(
389 &self,
390 x: Tensor, block: &BlockWeights,
392 scale_factor: f32,
393 is_last: bool,
394 scan_r: &mut Vec<f32>, scan_i: &mut Vec<f32>,
396 state_r: &mut Vec<f32>, state_i: &mut Vec<f32>, ) -> anyhow::Result<Tensor> {
399 let cfg = &self.config;
400 let state_dim = cfg.state_dim;
401 let embed_dim = cfg.embed_dim;
402
403 let seq_len = x.dim(0)?;
404
405 let skip = if is_last {
407 x.narrow(0, seq_len - 1, 1)? } else {
409 x.clone()
410 };
411
412 let (a_bar_r, a_bar_i, b_bar_r_t, b_bar_i_t) = {
414 let key = scale_factor.to_bits();
415 let mut cache = block.disc_cache.lock().unwrap();
416 if !cache.contains_key(&key) {
417 let result = discretize(&block.ssm, scale_factor, state_dim, embed_dim, &self.device)?;
418 cache.insert(key, result);
419 }
420 let (ar, ai, brt, bit) = &cache[&key];
421 (ar.clone(), ai.clone(), brt.clone(), bit.clone())
422 };
423
424 let bu_r = x.matmul(&b_bar_r_t.t()?)?; let bu_i = x.matmul(&b_bar_i_t.t()?)?;
427 let bu_r_data: Vec<f32> = bu_r.flatten_all()?.to_vec1()?;
428 let bu_i_data: Vec<f32> = bu_i.flatten_all()?.to_vec1()?;
429
430 state_r[..state_dim].fill(0.0);
437 state_i[..state_dim].fill(0.0);
438
439 let (h_r_t, h_i_t) = if is_last {
440 for t in 0..seq_len {
442 let bu_r_t = &bu_r_data[t * state_dim..(t + 1) * state_dim];
443 let bu_i_t = &bu_i_data[t * state_dim..(t + 1) * state_dim];
444 ssm_scan_step(
445 &mut state_r[..state_dim], &mut state_i[..state_dim],
446 &a_bar_r, &a_bar_i, bu_r_t, bu_i_t,
447 );
448 }
449 let hr = Tensor::from_vec(state_r[..state_dim].to_vec(), (1, state_dim), &self.device)?;
450 let hi = Tensor::from_vec(state_i[..state_dim].to_vec(), (1, state_dim), &self.device)?;
451 (hr, hi)
452 } else {
453 let needed = seq_len * state_dim;
457 if scan_r.len() < needed { scan_r.resize(needed, 0.0); }
458 if scan_i.len() < needed { scan_i.resize(needed, 0.0); }
459 for t in 0..seq_len {
460 let bu_r_t = &bu_r_data[t * state_dim..(t + 1) * state_dim];
461 let bu_i_t = &bu_i_data[t * state_dim..(t + 1) * state_dim];
462 ssm_scan_step(
463 &mut state_r[..state_dim], &mut state_i[..state_dim],
464 &a_bar_r, &a_bar_i, bu_r_t, bu_i_t,
465 );
466 let row = t * state_dim;
467 scan_r[row..row + state_dim].copy_from_slice(&state_r[..state_dim]);
468 scan_i[row..row + state_dim].copy_from_slice(&state_i[..state_dim]);
469 }
470 let hr = Tensor::from_vec(scan_r[..needed].to_vec(), (seq_len, state_dim), &self.device)?;
471 let hi = Tensor::from_vec(scan_i[..needed].to_vec(), (seq_len, state_dim), &self.device)?;
472 (hr, hi)
473 };
474
475 let y_from_cr = h_r_t.matmul(&block.ssm.c_r.t()?)?;
477 let y_from_ci = h_i_t.matmul(&block.ssm.c_i.t()?)?;
478 let y_raw = (y_from_cr - y_from_ci)?;
479
480 let y_t = (y_raw + skip.broadcast_mul(&block.ssm.d)?)?;
482
483 let y_selu = selu_tensor(&y_t)?;
485 let gate_pre = linear(&y_selu, &block.out_weight, &block.out_bias)?;
486 let gate = sigmoid_tensor(&gate_pre)?;
487 let y_gated = y_selu.mul(&gate)?;
488
489 let y_normed = layer_norm(&y_gated, &block.norm_weight, &block.norm_bias, self.config.eps)?;
491
492 Ok((y_normed + skip)?)
494 }
495}
496
497fn causal_revin_norm(x: &[f32], eps: f32) -> (Vec<f32>, f32, f32) {
504 let n = x.len();
505 let mut normed = vec![0.0f32; n];
506 let mut cum_sum = 0.0f32;
507 let mut cum_sq_diff = 0.0f32;
508 let mut final_mean = 0.0f32;
509 let mut final_std = 1.0f32;
510
511 for t in 0..n {
512 let count = (t + 1) as f32;
513 cum_sum += x[t];
514 let mean_t = cum_sum / count;
515
516 cum_sq_diff += (x[t] - mean_t) * (x[t] - mean_t);
517 let var_t = (cum_sq_diff / count).max(0.0);
518 let std_t = (var_t + eps).sqrt();
519
520 normed[t] = (x[t] - mean_t) / std_t;
521
522 if t == n - 1 {
523 final_mean = mean_t;
524 final_std = std_t;
525 }
526 }
527
528 (normed, final_mean, final_std)
529}
530
531fn discretize(
538 ssm: &S5Weights,
539 scale_factor: f32,
540 state_dim: usize,
541 embed_dim: usize,
542 device: &Device,
543) -> anyhow::Result<(Vec<f32>, Vec<f32>, Tensor, Tensor)> {
544 let mut a_r = vec![0.0f32; state_dim];
545 let mut a_i = vec![0.0f32; state_dim];
546 let mut coeff_r = vec![0.0f32; state_dim];
547 let mut coeff_i = vec![0.0f32; state_dim];
548
549 for s in 0..state_dim {
550 let lam_r = -ssm.log_lambda_real[s].exp();
551 let lam_i = ssm.lambda_imag[s];
552 let delta = scale_factor * ssm.log_delta[s].exp();
558
559 let exp_r = lam_r * delta;
560 let exp_i = lam_i * delta;
561 let mag = exp_r.exp();
562 a_r[s] = mag * exp_i.cos();
563 a_i[s] = mag * exp_i.sin();
564
565 let num_r = a_r[s] - 1.0;
566 let num_i = a_i[s];
567 let denom_sq = lam_r * lam_r + lam_i * lam_i;
568 if denom_sq > 1e-20 {
569 coeff_r[s] = (num_r * lam_r + num_i * lam_i) / denom_sq;
570 coeff_i[s] = (num_i * lam_r - num_r * lam_i) / denom_sq;
571 } else {
572 coeff_r[s] = delta;
573 coeff_i[s] = 0.0;
574 }
575 }
576
577 let b_r_data = get_tensor_data_row_major(&ssm.b_r, state_dim, embed_dim);
578 let b_i_data = get_tensor_data_row_major(&ssm.b_i, state_dim, embed_dim);
579
580 let mut b_bar_r = vec![0.0f32; state_dim * embed_dim];
581 let mut b_bar_i = vec![0.0f32; state_dim * embed_dim];
582
583 for s in 0..state_dim {
584 for e in 0..embed_dim {
585 let br = b_r_data[s * embed_dim + e];
586 let bi = b_i_data[s * embed_dim + e];
587 b_bar_r[s * embed_dim + e] = coeff_r[s] * br - coeff_i[s] * bi;
588 b_bar_i[s * embed_dim + e] = coeff_r[s] * bi + coeff_i[s] * br;
589 }
590 }
591
592 let b_bar_r_t = Tensor::from_vec(b_bar_r, (state_dim, embed_dim), device)?;
593 let b_bar_i_t = Tensor::from_vec(b_bar_i, (state_dim, embed_dim), device)?;
594
595 Ok((a_r, a_i, b_bar_r_t, b_bar_i_t))
596}
597
598fn get_tensor_data_row_major(t: &Tensor, rows: usize, cols: usize) -> Vec<f32> {
600 t.to_dtype(DType::F32)
601 .and_then(|t| t.reshape((rows, cols)))
602 .and_then(|t| t.flatten_all())
603 .and_then(|t| t.to_vec1())
604 .unwrap_or_else(|_| vec![0.0f32; rows * cols])
605}
606
607pub fn dump_legendre_basis(n_points: usize, degree: usize, range: [f32; 2],
613 scale: f32, pred_dist: usize) -> Vec<Vec<f32>> {
614 legendre_basis(n_points, degree, range, scale, pred_dist)
615}
616
617fn legendre_basis(n_points: usize, degree: usize, range: [f32; 2],
620 scale: f32, pred_dist: usize) -> Vec<Vec<f32>> {
621 let dt = scale * (range[1] - range[0]) / pred_dist as f32;
622 let t: Vec<f32> = (1..=n_points).map(|i| range[0] + i as f32 * dt).collect();
623
624 let mut basis = vec![vec![0.0f32; degree + 1]; n_points];
627 for (p, &x) in t.iter().enumerate() {
628 basis[p][0] = 1.0;
629 if degree >= 1 {
630 basis[p][1] = x;
631 }
632 for k in 1..degree {
633 let kf = k as f32;
634 basis[p][k + 1] =
635 ((2.0 * kf + 1.0) * x * basis[p][k] - kf * basis[p][k - 1]) / (kf + 1.0);
636 }
637 for d in 0..degree {
638 basis[p][d] /= 4.0;
639 }
640 basis[p].truncate(degree);
641 }
642
643 basis
644}
645
646fn linear(x: &Tensor, w: &Tensor, b: &Tensor) -> anyhow::Result<Tensor> {
652 zsfm_nn::linear_bias(x, w, b)
653}
654
655fn selu_tensor(x: &Tensor) -> anyhow::Result<Tensor> {
657 const SCALE: f64 = 1.0507009873554804934193349852946;
658 const ALPHA: f64 = 1.6732632423543772848170429916717;
659 const ALPHA_SCALE: f64 = SCALE * ALPHA;
660 let pos = x.relu()?;
661 let neg = (x - &pos)?; let selu_pos = (pos * SCALE)?;
663 let selu_neg = ((neg.exp()? - 1.0)? * ALPHA_SCALE)?;
664 Ok((selu_pos + selu_neg)?)
665}
666
667fn sigmoid_tensor(x: &Tensor) -> anyhow::Result<Tensor> {
668 Ok(ops::sigmoid(x)?)
669}
670
671fn layer_norm(x: &Tensor, weight: &Tensor, bias: &Tensor, eps: f32) -> anyhow::Result<Tensor> {
673 zsfm_nn::layer_norm(x, weight, bias, eps as f64)
674}
675
676impl zsfm_core::Forecaster for FlowStateModel {
681 type Config = InferConfig;
682
683 fn load(gguf_path: &Path, config: InferConfig) -> Result<Self> {
684 FlowStateModel::load(gguf_path, config)
685 }
686
687 fn forecast(
691 &self,
692 context: &[Vec<f32>],
693 _mask: &[Vec<bool>],
694 horizon: usize,
695 ) -> Result<zsfm_core::QuantileMatrix> {
696 anyhow::ensure!(context.len() == 1, "FlowStateModel only supports univariate forecasting (1 variate)");
697 let qmat = FlowStateModel::forecast(self, &context[0], horizon)?; Ok(qmat.into_iter().map(|row| vec![row]).collect())
699 }
700}