1use std::path::Path;
11
12use anyhow::{Context, Result};
13use burn::tensor::{activation, Device, Tensor, TensorData};
14use simdeez::math::{SimdMathF32Core, SimdMathF32Hyperbolic};
15use simdeez::prelude::*;
16use zsfm_burn::linear::burn_linear_nd;
17use zsfm_burn::{open_gguf_weights, GgufWeights};
18
19use crate::config::TiRexConfig;
20
21simd_runtime_generate!(
26 fn simd_sq_sum(row: &[f32]) -> f32 {
27 let mut r = &row[..];
28 let mut acc = S::Vf32::zeroes();
29 while r.len() >= S::Vf32::WIDTH {
30 let v = S::Vf32::load_from_slice(r);
31 acc = v.mul_add(v, acc);
32 r = &r[S::Vf32::WIDTH..];
33 }
34 let mut sum = acc.horizontal_add();
35 for &x in r {
36 sum += x * x;
37 }
38 sum
39 }
40);
41
42simd_runtime_generate!(
43 fn simd_scale_weight(row: &mut [f32], w: &[f32], scale: f32) {
44 let sc = S::Vf32::set1(scale);
45 let mut r = &mut row[..];
46 let mut wv = &w[..];
47 while r.len() >= S::Vf32::WIDTH {
48 let v = S::Vf32::load_from_slice(r);
49 let wi = S::Vf32::load_from_slice(wv);
50 (v * sc * wi).copy_to_slice(r);
51 r = &mut r[S::Vf32::WIDTH..];
52 wv = &wv[S::Vf32::WIDTH..];
53 }
54 for i in 0..r.len() {
55 r[i] *= scale * wv[i];
56 }
57 }
58);
59
60simd_runtime_generate!(
61 fn simd_dot(a: &[f32], b: &[f32]) -> f32 {
62 let mut aa = &a[..];
63 let mut bb = &b[..];
64 let mut acc = S::Vf32::zeroes();
65 while aa.len() >= S::Vf32::WIDTH {
66 let va = S::Vf32::load_from_slice(aa);
67 let vb = S::Vf32::load_from_slice(bb);
68 acc = va.mul_add(vb, acc);
69 aa = &aa[S::Vf32::WIDTH..];
70 bb = &bb[S::Vf32::WIDTH..];
71 }
72 let mut sum = acc.horizontal_add();
73 for (&x, &y) in aa.iter().zip(bb.iter()) {
74 sum += x * y;
75 }
76 sum
77 }
78);
79
80simd_runtime_generate!(
81 fn simd_gate_update(
82 iraw: &[f32],
83 fraw: &[f32],
84 zraw: &[f32],
85 oraw: &[f32],
86 c: &[f32],
87 n: &[f32],
88 m: &[f32],
89 cnew: &mut [f32],
90 nnew: &mut [f32],
91 hnew: &mut [f32],
92 mnew: &mut [f32],
93 is_first: bool,
94 ) {
95 let clamp = S::Vf32::set1(15.0f32);
96 let one = S::Vf32::set1(1.0f32);
97 let eps = S::Vf32::set1(1e-8f32);
98 let zero = S::Vf32::zeroes();
99
100 let mut ir = &iraw[..];
101 let mut fr = &fraw[..];
102 let mut zr = &zraw[..];
103 let mut or_ = &oraw[..];
104 let mut cv = &c[..];
105 let mut nv = &n[..];
106 let mut mv = &m[..];
107 let mut cnw = &mut cnew[..];
108 let mut nnw = &mut nnew[..];
109 let mut hnw = &mut hnew[..];
110 let mut mnw = &mut mnew[..];
111
112 while ir.len() >= S::Vf32::WIDTH {
113 let iv = S::Vf32::load_from_slice(ir);
114 let fv = S::Vf32::load_from_slice(fr).min(clamp);
115 let zv = S::Vf32::load_from_slice(zr);
116 let ov = S::Vf32::load_from_slice(or_);
117 let cv_ = S::Vf32::load_from_slice(cv);
118 let nv_ = S::Vf32::load_from_slice(nv);
119 let mv_ = S::Vf32::load_from_slice(mv);
120
121 let ls_pos = -(one + (-fv).exp_u35()).ln_u35(); let ls_neg = fv - (one + fv.exp_u35()).ln_u35(); let log_sig_f = fv.cmp_lt(zero).blendv(ls_pos, ls_neg);
125
126 let logfplusm = mv_ + log_sig_f;
127 let mnew_v = if is_first { iv } else { iv.max(logfplusm) };
128
129 let ogate = one / (one + (-ov).exp_u35()); let igate = (iv - mnew_v).exp_u35().min(one);
131 let fgate = (logfplusm - mnew_v).exp_u35().min(one);
132 let zgate = zv.tanh_u35();
133
134 let cnew_v = fgate.mul_add(cv_, igate * zgate);
135 let nnew_v = fgate.mul_add(nv_, igate);
136
137 let mask = nnew_v.abs().cmp_gt(eps);
138 let hnew_v = mask.blendv(zero, ogate * cnew_v / nnew_v);
139
140 cnew_v.copy_to_slice(cnw);
141 nnew_v.copy_to_slice(nnw);
142 hnew_v.copy_to_slice(hnw);
143 mnew_v.copy_to_slice(mnw);
144
145 ir = &ir[S::Vf32::WIDTH..];
146 fr = &fr[S::Vf32::WIDTH..];
147 zr = &zr[S::Vf32::WIDTH..];
148 or_ = &or_[S::Vf32::WIDTH..];
149 cv = &cv[S::Vf32::WIDTH..];
150 nv = &nv[S::Vf32::WIDTH..];
151 mv = &mv[S::Vf32::WIDTH..];
152 cnw = &mut cnw[S::Vf32::WIDTH..];
153 nnw = &mut nnw[S::Vf32::WIDTH..];
154 hnw = &mut hnw[S::Vf32::WIDTH..];
155 mnw = &mut mnw[S::Vf32::WIDTH..];
156 }
157
158 for i in 0..ir.len() {
159 let iv_s = ir[i];
160 let fv_s = fr[i].min(15.0);
161 let zv_s = zr[i];
162 let ov_s = or_[i];
163 let c_s = cv[i];
164 let n_s = nv[i];
165 let m_s = mv[i];
166 let ls = if fv_s >= 0.0 {
167 -(1.0 + (-fv_s).exp()).ln()
168 } else {
169 fv_s - (1.0 + fv_s.exp()).ln()
170 };
171 let lfpm = m_s + ls;
172 let mn = if is_first { iv_s } else { iv_s.max(lfpm) };
173 let og = 1.0 / (1.0 + (-ov_s).exp());
174 let ig = (iv_s - mn).exp().min(1.0);
175 let fg = (lfpm - mn).exp().min(1.0);
176 let zg = zv_s.tanh();
177 cnw[i] = fg * c_s + ig * zg;
178 nnw[i] = fg * n_s + ig;
179 hnw[i] = if nnw[i].abs() > 1e-8 {
180 og * cnw[i] / nnw[i]
181 } else {
182 0.0
183 };
184 mnw[i] = mn;
185 }
186 }
187);
188
189fn standard_scaler(x: &[f32]) -> (f32, f32) {
191 let eps = 1e-5f32;
192 let valid: Vec<f32> = x.iter().copied().filter(|v| !v.is_nan()).collect();
193 if valid.is_empty() {
194 return (0.0, 1.0);
195 }
196 let loc = valid.iter().sum::<f32>() / valid.len() as f32;
197 let variance = valid.iter().map(|&v| (v - loc) * (v - loc)).sum::<f32>() / valid.len() as f32;
198 let scale = variance.sqrt();
199 let scale = if scale == 0.0 { loc.abs() + eps } else { scale };
200 (loc, scale)
201}
202
203fn adjust_context(x: &[f32], target_len: usize) -> Vec<f32> {
205 if x.len() >= target_len {
206 x[x.len() - target_len..].to_vec()
207 } else {
208 let pad = target_len - x.len();
209 let mut out = vec![f32::NAN; target_len];
210 out[pad..].copy_from_slice(x);
211 out
212 }
213}
214
215fn rms_norm_inplace(x: &mut [f32], w: &[f32], d: usize, eps: f32) {
217 let s = x.len() / d;
218 for t in 0..s {
219 let row = &mut x[t * d..(t + 1) * d];
220 let ss = simd_sq_sum(row);
221 let scale = 1.0 / (ss / d as f32 + eps).sqrt();
222 simd_scale_weight(row, w, scale);
223 }
224}
225
226fn headwise_linear_batch_ng(
233 x: &[f32],
234 w: &[f32],
235 s: usize,
236 nh: usize,
237 dh: usize,
238 ng: usize,
239 out: &mut [f32],
240) {
241 let d = nh * dh;
242 let ngdh = ng * dh;
243 for t in 0..s {
244 for h in 0..nh {
245 let x_h = &x[t * d + h * dh..t * d + (h + 1) * dh];
246 let w_h_off = h * ngdh * dh;
247 for g in 0..ng {
248 let out_base = t * ng * d + g * d + h * dh;
249 let w_gate_off = w_h_off + g * dh * dh;
250 for o in 0..dh {
251 let w_row = &w[w_gate_off + o * dh..w_gate_off + (o + 1) * dh];
252 out[out_base + o] = simd_dot(x_h, w_row);
253 }
254 }
255 }
256 }
257}
258
259fn compute_ry(
265 h: &[f32],
266 kernel_t: &[f32],
267 nh: usize,
268 dh: usize,
269 ng: usize,
270 ry_raw: &mut [f32],
271 out: &mut [f32],
272) {
273 for head in 0..nh {
275 let h_h = &h[head * dh..(head + 1) * dh];
276 let k_head = &kernel_t[head * ng * dh * dh..];
277 for gate_d in 0..ng * dh {
278 let k_row = &k_head[gate_d * dh..(gate_d + 1) * dh]; ry_raw[head * ng * dh + gate_d] = simd_dot(h_h, k_row);
280 }
281 }
282 for head in 0..nh {
284 for g in 0..ng {
285 for d in 0..dh {
286 out[g * nh * dh + head * dh + d] = ry_raw[head * ng * dh + g * dh + d];
287 }
288 }
289 }
290}
291
292fn device() -> Device {
293 Device::flex()
294}
295
296fn load_t(weights: &GgufWeights, name: &str) -> Result<Tensor<2>> {
297 weights.load_2d(name)
298}
299
300fn load_vec(weights: &GgufWeights, name: &str) -> Result<Vec<f32>> {
301 weights.load_flat(name)
302}
303
304fn to_host<const D: usize>(t: &Tensor<D>) -> Result<Vec<f32>> {
305 t.to_data()
306 .try_to_vec()
307 .map_err(|e| anyhow::anyhow!("{e:?}"))
308}
309
310struct BurnEmbedBlock {
311 hidden_w: Tensor<2>,
312 hidden_b: Vec<f32>,
313 output_w: Tensor<2>,
314 output_b: Vec<f32>,
315 residual_w: Tensor<2>,
316 residual_b: Vec<f32>,
317}
318
319struct BurnBlock {
320 norm_slstm: Vec<f32>,
321 fizo_w: Vec<f32>,
322 slstm_kernel_t: Vec<f32>,
323 slstm_bias: Vec<f32>,
324 group_norm_w: Vec<f32>,
325 norm_ffn: Vec<f32>,
326 ffn_gate_w: Tensor<2>,
327 ffn_up_w: Tensor<2>,
328 ffn_down_w: Tensor<2>,
329}
330
331pub struct BurnTiRexModel {
332 config: TiRexConfig,
333 in_emb: BurnEmbedBlock,
334 blocks: Vec<BurnBlock>,
335 out_norm: Vec<f32>,
336 out_emb: BurnEmbedBlock,
337}
338
339fn load_embed_block(weights: &GgufWeights, prefix: &str) -> Result<BurnEmbedBlock> {
340 let p = |s: &str| format!("{prefix}.{s}");
341 Ok(BurnEmbedBlock {
342 hidden_w: load_t(weights, &p("hidden.weight"))?,
343 hidden_b: load_vec(weights, &p("hidden.bias"))?,
344 output_w: load_t(weights, &p("output.weight"))?,
345 output_b: load_vec(weights, &p("output.bias"))?,
346 residual_w: load_t(weights, &p("residual.weight"))?,
347 residual_b: load_vec(weights, &p("residual.bias"))?,
348 })
349}
350
351impl BurnTiRexModel {
352 pub fn load(gguf_path: &Path, config: TiRexConfig) -> Result<Self> {
353 let weights = open_gguf_weights(gguf_path)
354 .with_context(|| format!("open {}", gguf_path.display()))?;
355
356 let in_emb = load_embed_block(&weights, "in_emb")?;
357 let nh = config.num_heads;
358 let dh = config.head_dim();
359 let wpp = dh * dh;
360 let mut blocks = Vec::with_capacity(config.num_blocks);
361 for n in 0..config.num_blocks {
362 let p = |s: &str| format!("blk.{n}.{s}");
363 let fgate_w = load_vec(&weights, &p("fgate.weight"))?;
364 let igate_w = load_vec(&weights, &p("igate.weight"))?;
365 let zgate_w = load_vec(&weights, &p("zgate.weight"))?;
366 let ogate_w = load_vec(&weights, &p("ogate.weight"))?;
367 let mut fizo_w = vec![0.0f32; nh * 4 * wpp];
368 for h in 0..nh {
369 fizo_w[h * 4 * wpp..h * 4 * wpp + wpp]
370 .copy_from_slice(&fgate_w[h * wpp..(h + 1) * wpp]);
371 fizo_w[h * 4 * wpp + wpp..h * 4 * wpp + 2 * wpp]
372 .copy_from_slice(&igate_w[h * wpp..(h + 1) * wpp]);
373 fizo_w[h * 4 * wpp + 2 * wpp..h * 4 * wpp + 3 * wpp]
374 .copy_from_slice(&zgate_w[h * wpp..(h + 1) * wpp]);
375 fizo_w[h * 4 * wpp + 3 * wpp..h * 4 * wpp + 4 * wpp]
376 .copy_from_slice(&ogate_w[h * wpp..(h + 1) * wpp]);
377 }
378 let raw_kernel = load_vec(&weights, &p("slstm_kernel"))?;
379 let ng = 4usize;
380 let mut slstm_kernel_t = vec![0.0f32; nh * ng * dh * dh];
381 for head in 0..nh {
382 for gate_d in 0..(ng * dh) {
383 for di in 0..dh {
384 slstm_kernel_t[head * ng * dh * dh + gate_d * dh + di] =
385 raw_kernel[head * dh * ng * dh + di * ng * dh + gate_d];
386 }
387 }
388 }
389 blocks.push(BurnBlock {
390 norm_slstm: load_vec(&weights, &p("norm_slstm"))?,
391 fizo_w,
392 slstm_kernel_t,
393 slstm_bias: load_vec(&weights, &p("slstm_bias"))?,
394 group_norm_w: load_vec(&weights, &p("group_norm"))?,
395 norm_ffn: load_vec(&weights, &p("norm_ffn"))?,
396 ffn_gate_w: load_t(&weights, &p("ffn_gate.weight"))?,
397 ffn_up_w: load_t(&weights, &p("ffn_up.weight"))?,
398 ffn_down_w: load_t(&weights, &p("ffn_down.weight"))?,
399 });
400 }
401 let out_norm = load_vec(&weights, "out_norm")?;
402 let out_emb = load_embed_block(&weights, "out_emb")?;
403 Ok(Self {
404 config,
405 in_emb,
406 blocks,
407 out_norm,
408 out_emb,
409 })
410 }
411}
412
413fn residual_block_forward_burn(
415 x: &[f32],
416 s: usize,
417 in_dim: usize,
418 h_dim: usize,
419 out_dim: usize,
420 emb: &BurnEmbedBlock,
421) -> Vec<f32> {
422 let dev = device();
423 let xt = Tensor::<2>::from_data(TensorData::new(x.to_vec(), [s, in_dim]), &dev);
424 let bh = Tensor::<1>::from_data(TensorData::new(emb.hidden_b.clone(), [h_dim]), &dev);
425 let h = activation::relu(burn_linear_nd(xt.clone(), emb.hidden_w.clone(), Some(bh)));
426 let bo = Tensor::<1>::from_data(TensorData::new(emb.output_b.clone(), [out_dim]), &dev);
427 let out = burn_linear_nd(h, emb.output_w.clone(), Some(bo));
428 let br = Tensor::<1>::from_data(TensorData::new(emb.residual_b.clone(), [out_dim]), &dev);
429 let res = burn_linear_nd(xt, emb.residual_w.clone(), Some(br));
430 let y: Vec<f32> = (out + res).to_data().try_to_vec().unwrap();
431 y
432}
433
434fn ffn_forward_burn(
436 x: &[f32],
437 s: usize,
438 in_dim: usize,
439 up_dim: usize,
440 blk: &BurnBlock,
441) -> Vec<f32> {
442 let dev = device();
443 let xt = Tensor::<2>::from_data(TensorData::new(x.to_vec(), [s, in_dim]), &dev);
444 let gate = burn_linear_nd(xt.clone(), blk.ffn_gate_w.clone(), None);
445 let up = burn_linear_nd(xt, blk.ffn_up_w.clone(), None);
446 let h = activation::silu(gate) * up;
447 let out = burn_linear_nd(h, blk.ffn_down_w.clone(), None);
448 out.to_data().try_to_vec().unwrap()
449}
450
451impl BurnTiRexModel {
452 pub fn forecast(
453 &self,
454 context: &[f32],
455 prediction_length: usize,
456 ) -> Result<(Vec<Vec<f32>>, Vec<f32>)> {
457 let cfg = &self.config;
458 let patch_size = cfg.patch_size;
459 let n_quantiles = cfg.num_quantiles;
460 let median_idx = cfg
461 .quantiles
462 .iter()
463 .position(|&q| (q - 0.5).abs() < 1e-6)
464 .unwrap_or(4);
465 let n_steps = prediction_length.div_ceil(patch_size);
466 let mut all_q: Vec<f32> = Vec::with_capacity(n_steps * patch_size * n_quantiles);
467 let mut ctx: Vec<f32> = context.to_vec();
468 for _ in 0..n_steps {
469 let full_ctx = adjust_context(&ctx, cfg.train_ctx_len);
470 let (loc, scale) = standard_scaler(&full_ctx);
471 let s_vals: Vec<f32> = full_ctx
472 .iter()
473 .map(|&x| {
474 if x.is_nan() {
475 0.0f32
476 } else {
477 (x - loc) / scale
478 }
479 })
480 .collect();
481 let s_mask: Vec<f32> = full_ctx
482 .iter()
483 .map(|&x| if x.is_nan() { 0.0f32 } else { 1.0f32 })
484 .collect();
485 let num_patches = cfg.train_ctx_len / patch_size;
486 let mut patched_vals = vec![0.0f32; num_patches * patch_size];
487 let mut patched_mask = vec![0.0f32; num_patches * patch_size];
488 for p in 0..num_patches {
489 let start = p * patch_size;
490 patched_vals[p * patch_size..(p + 1) * patch_size]
491 .copy_from_slice(&s_vals[start..start + patch_size]);
492 patched_mask[p * patch_size..(p + 1) * patch_size]
493 .copy_from_slice(&s_mask[start..start + patch_size]);
494 }
495 let in_dim = patch_size * 2;
496 let mut x_in = vec![0.0f32; num_patches * in_dim];
497 for p in 0..num_patches {
498 x_in[p * in_dim..p * in_dim + patch_size]
499 .copy_from_slice(&patched_vals[p * patch_size..(p + 1) * patch_size]);
500 x_in[p * in_dim + patch_size..p * in_dim + in_dim]
501 .copy_from_slice(&patched_mask[p * patch_size..(p + 1) * patch_size]);
502 }
503 let mut hidden = residual_block_forward_burn(
504 &x_in,
505 num_patches,
506 in_dim,
507 cfg.input_ff_dim,
508 cfg.embedding_dim,
509 &self.in_emb,
510 );
511 let ng = 4usize;
512 let d = cfg.embedding_dim;
513 let mut sc_xg = vec![0.0f32; num_patches * ng * d];
514 let mut sc_hout = vec![0.0f32; num_patches * d];
515 let mut sc_y = vec![0.0f32; num_patches * d];
516 let mut sc_xn = vec![0.0f32; num_patches * d];
517 let mut sc_raw = vec![0.0f32; ng * d];
518 let mut sc_ry_raw = vec![0.0f32; ng * d];
519 let mut sc_ry_out = vec![0.0f32; ng * d];
520 let mut sc_hnew = vec![0.0f32; d];
521 let mut sc_cnew = vec![0.0f32; d];
522 let mut sc_nnew = vec![0.0f32; d];
523 let mut sc_mnew = vec![0.0f32; d];
524 for block in &self.blocks {
525 hidden = self.forward_slstm_block(
526 &hidden,
527 num_patches,
528 block,
529 &mut sc_xg,
530 &mut sc_hout,
531 &mut sc_y,
532 &mut sc_xn,
533 &mut sc_raw,
534 &mut sc_ry_raw,
535 &mut sc_ry_out,
536 &mut sc_hnew,
537 &mut sc_cnew,
538 &mut sc_nnew,
539 &mut sc_mnew,
540 )?;
541 }
542 rms_norm_inplace(&mut hidden, &self.out_norm, cfg.embedding_dim, 1e-6);
543 let out_dim = cfg.output_dim();
544 let preds = residual_block_forward_burn(
545 &hidden,
546 num_patches,
547 cfg.embedding_dim,
548 cfg.input_ff_dim,
549 out_dim,
550 &self.out_emb,
551 );
552 let last = &preds[(num_patches - 1) * out_dim..num_patches * out_dim];
553 for q in 0..n_quantiles {
554 for d in 0..patch_size {
555 let v = last[q * patch_size + d] * scale + loc;
556 all_q.push(v);
557 }
558 }
559 ctx.extend(std::iter::repeat(f32::NAN).take(patch_size));
560 }
561 let total = n_steps * patch_size;
562 let mut quantiles: Vec<Vec<f32>> = vec![Vec::with_capacity(prediction_length); n_quantiles];
563 for t in 0..prediction_length.min(total) {
564 let step = t / patch_size;
565 let d = t % patch_size;
566 for q in 0..n_quantiles {
567 quantiles[q].push(all_q[step * n_quantiles * patch_size + q * patch_size + d]);
568 }
569 }
570 let median: Vec<f32> = (0..prediction_length)
571 .map(|t| quantiles[median_idx][t])
572 .collect();
573 Ok((quantiles, median))
574 }
575
576 #[allow(clippy::too_many_arguments)]
577 fn forward_slstm_block(
578 &self,
579 x: &[f32],
580 s: usize,
581 blk: &BurnBlock,
582 sc_xg: &mut [f32],
583 sc_hout: &mut [f32],
584 sc_y: &mut [f32],
585 sc_xn: &mut [f32],
586 sc_raw: &mut [f32],
587 sc_ry_raw: &mut [f32],
588 sc_ry_out: &mut [f32],
589 sc_hnew: &mut Vec<f32>,
590 sc_cnew: &mut Vec<f32>,
591 sc_nnew: &mut Vec<f32>,
592 sc_mnew: &mut Vec<f32>,
593 ) -> Result<Vec<f32>> {
594 let cfg = &self.config;
595 let d = cfg.embedding_dim;
596 let nh = cfg.num_heads;
597 let dh = cfg.head_dim();
598 let ng = 4usize;
599 sc_xn[..s * d].copy_from_slice(&x[..s * d]);
600 rms_norm_inplace(&mut sc_xn[..s * d], &blk.norm_slstm, d, 1e-6);
601 headwise_linear_batch_ng(&sc_xn[..s * d], &blk.fizo_w, s, nh, dh, ng, sc_xg);
602 let mut h = vec![0.0f32; d];
603 let mut c = vec![0.0f32; d];
604 let mut n = vec![0.0f32; d];
605 let mut m = vec![f32::NEG_INFINITY; d];
606 for t in 0..s {
607 let wx = &sc_xg[t * ng * d..(t + 1) * ng * d];
608 compute_ry(&h, &blk.slstm_kernel_t, nh, dh, ng, sc_ry_raw, sc_ry_out);
609 for i in 0..ng * d {
610 sc_raw[i] = wx[i] + sc_ry_out[i] + blk.slstm_bias[i];
611 }
612 let is_first = t == 0;
613 simd_gate_update(
614 &sc_raw[..d],
615 &sc_raw[d..2 * d],
616 &sc_raw[2 * d..3 * d],
617 &sc_raw[3 * d..],
618 &c,
619 &n,
620 &m,
621 sc_cnew,
622 sc_nnew,
623 sc_hnew,
624 sc_mnew,
625 is_first,
626 );
627 std::mem::swap(&mut h, sc_hnew);
628 std::mem::swap(&mut c, sc_cnew);
629 std::mem::swap(&mut n, sc_nnew);
630 std::mem::swap(&mut m, sc_mnew);
631 sc_hout[t * d..(t + 1) * d].copy_from_slice(&h);
632 }
633 for t in 0..s {
634 let h_t = &sc_hout[t * d..(t + 1) * d];
635 for head in 0..nh {
636 let start = head * dh;
637 let h_slice = &h_t[start..start + dh];
638 let mean = h_slice.iter().sum::<f32>() / dh as f32;
639 let var = h_slice
640 .iter()
641 .map(|&v| (v - mean) * (v - mean))
642 .sum::<f32>()
643 / dh as f32;
644 let inv_std = 1.0 / (var + 1e-5f32).sqrt();
645 let w_slice = &blk.group_norm_w[start..start + dh];
646 for d_i in 0..dh {
647 sc_y[t * d + start + d_i] =
648 (h_t[start + d_i] - mean) * inv_std * (1.0 + w_slice[d_i]);
649 }
650 }
651 }
652 let mut x_out = x.to_vec();
653 for i in 0..s * d {
654 x_out[i] += sc_y[i];
655 }
656 sc_xn[..s * d].copy_from_slice(&x_out[..s * d]);
657 rms_norm_inplace(&mut sc_xn[..s * d], &blk.norm_ffn, d, 1e-6);
658 let ffn_out = ffn_forward_burn(&sc_xn[..s * d], s, d, cfg.ffn_up_dim, blk);
659 for i in 0..s * d {
660 x_out[i] += ffn_out[i];
661 }
662 Ok(x_out)
663 }
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669 use zsfm_gguf::{GGMLType, GGUFMetaValue, GGUFWriter};
670
671 fn pseudo(n: usize, seed: u64) -> Vec<f32> {
672 let mut x = seed;
673 (0..n)
674 .map(|_| {
675 x = x
676 .wrapping_mul(6364136223846793005)
677 .wrapping_add(1442695040888963407);
678 ((x >> 33) as f32 / u32::MAX as f32) * 2.0 - 1.0
679 })
680 .collect()
681 }
682
683 fn tiny_config() -> TiRexConfig {
684 TiRexConfig {
685 patch_size: 4,
686 num_blocks: 1,
687 embedding_dim: 16,
688 num_heads: 2,
689 input_ff_dim: 32,
690 ffn_up_dim: 24,
691 train_ctx_len: 16,
692 quantiles: vec![0.25, 0.5, 0.75],
693 num_quantiles: 3,
694 }
695 }
696
697 fn put2(w: &mut GGUFWriter, name: &str, rows: usize, cols: usize, seed: &mut u64, scale: f32) {
698 *seed += 1;
699 let data: Vec<f32> = pseudo(rows * cols, *seed)
700 .iter()
701 .map(|v| v * scale)
702 .collect();
703 let bytes: Vec<u8> = data.iter().flat_map(|v| v.to_le_bytes()).collect();
704 w.add_tensor(name, vec![cols as u64, rows as u64], GGMLType::F32, bytes);
705 }
706
707 fn put1(w: &mut GGUFWriter, name: &str, n: usize, seed: &mut u64) {
708 *seed += 1;
709 let bytes: Vec<u8> = pseudo(n, *seed)
710 .iter()
711 .flat_map(|v| v.to_le_bytes())
712 .collect();
713 w.add_tensor(name, vec![n as u64], GGMLType::F32, bytes);
714 }
715
716 fn put_emb(
717 w: &mut GGUFWriter,
718 prefix: &str,
719 h: usize,
720 d_in: usize,
721 o: usize,
722 seed: &mut u64,
723 s: f32,
724 ) {
725 put2(w, &format!("{prefix}.hidden.weight"), h, d_in, seed, s);
726 put1(w, &format!("{prefix}.hidden.bias"), h, seed);
727 put2(w, &format!("{prefix}.output.weight"), o, h, seed, s);
728 put1(w, &format!("{prefix}.output.bias"), o, seed);
729 put2(w, &format!("{prefix}.residual.weight"), o, d_in, seed, s);
730 put1(w, &format!("{prefix}.residual.bias"), o, seed);
731 }
732
733 #[test]
734 fn burn_forecast_golden() {
735 let dir = std::env::temp_dir().join(format!("tirex-burn-{}", std::process::id()));
736 let _ = std::fs::remove_dir_all(&dir);
737 std::fs::create_dir_all(&dir).unwrap();
738 let path = dir.join("model.gguf");
739 let mut w = GGUFWriter::new();
740 w.add_metadata(
741 "general.architecture",
742 GGUFMetaValue::String("tirex".into()),
743 );
744 let mut seed = 2900u64;
745 let s = 0.05;
746 put_emb(&mut w, "in_emb", 32, 8, 16, &mut seed, s);
747 for t in ["fgate", "igate", "zgate", "ogate"] {
748 put2(&mut w, &format!("blk.0.{t}.weight"), 16, 16, &mut seed, s);
749 }
750 put1(&mut w, "blk.0.slstm_kernel", 2 * 8 * 32, &mut seed);
752 put1(&mut w, "blk.0.slstm_bias", 64, &mut seed);
754 put1(&mut w, "blk.0.norm_slstm", 16, &mut seed);
755 put1(&mut w, "blk.0.group_norm", 16, &mut seed);
756 put1(&mut w, "blk.0.norm_ffn", 16, &mut seed);
757 put2(&mut w, "blk.0.ffn_gate.weight", 24, 16, &mut seed, s);
758 put2(&mut w, "blk.0.ffn_up.weight", 24, 16, &mut seed, s);
759 put2(&mut w, "blk.0.ffn_down.weight", 16, 24, &mut seed, s);
760 put1(&mut w, "out_norm", 16, &mut seed);
761 put_emb(&mut w, "out_emb", 32, 16, 12, &mut seed, s);
762 let f = std::fs::File::create(&path).unwrap();
763 w.write_to(&mut std::io::BufWriter::new(f)).unwrap();
764
765 let cfg = tiny_config();
766 let burn = BurnTiRexModel::load(&path, cfg).unwrap();
767 let ctx: Vec<f32> = (0..8).map(|i| 10.0 + 0.5 * i as f32).collect();
768 let t0 = std::time::Instant::now();
769 let (aq, am) = burn.forecast(&ctx, 2).unwrap();
770 let burn_ms = t0.elapsed().as_secs_f64() * 1000.0;
771 let (bq, bm) = burn.forecast(&ctx, 2).unwrap();
772 assert_eq!(aq, bq, "quantiles must be deterministic");
773 assert_eq!(am, bm, "median must be deterministic");
774 assert!(aq.iter().all(|r| r.iter().all(|v| v.is_finite())));
775 assert!(am.iter().all(|v| v.is_finite()));
776 println!("tirex synthetic: burn {burn_ms:.1}ms q={aq:?} m={am:?}");
777 let golden_q: Vec<Vec<f32>> = vec![
779 vec![10.033624, 10.955177],
780 vec![10.487212, 9.479774],
781 vec![10.795641, 10.196359],
782 ];
783 let golden_m: Vec<f32> = vec![10.487212, 9.479774];
784 assert_eq!(aq.len(), golden_q.len());
785 assert_eq!(am.len(), golden_m.len());
786 for (ra, re) in aq.iter().zip(golden_q.iter()) {
787 for (x, y) in ra.iter().zip(re.iter()) {
788 assert!((x - y).abs() < 1e-3, "golden drift: {x} vs {y}");
789 }
790 }
791 for (x, y) in am.iter().zip(golden_m.iter()) {
792 assert!((x - y).abs() < 1e-3, "golden drift: {x} vs {y}");
793 }
794 let _ = std::fs::remove_dir_all(&dir);
795 }
796}