1use std::path::Path;
11
12use anyhow::{bail, Context, Result};
13use burn::tensor::{activation, Device, Tensor, TensorData};
14use zsfm_burn::linear::burn_linear_nd;
15use zsfm_burn::{open_gguf_weights, GgufWeights};
16use zsfm_core::{json_bool, json_f64, json_usize};
17
18const SILU_SCALE: f32 = 1.766782948312328;
19
20#[derive(Clone, Debug)]
25pub struct InferConfig {
26 d_model: usize,
27 num_layers: usize,
28 num_heads: usize,
29 num_groups: usize,
30 qk_dim: usize,
31 v_dim: usize,
32 patch_size: usize,
33 norm_eps: f64,
34 layer_group_size: usize,
35 num_variate_layers_per_group: usize,
36 variate_layer_first: bool,
37 use_xpos: bool,
38 residual_mult: f64,
39 residual_attn_ratio: f64,
40 compute_f64: bool,
43}
44
45impl Default for InferConfig {
46 fn default() -> Self {
48 InferConfig {
49 d_model: 2048,
50 num_layers: 48,
51 num_heads: 32,
52 num_groups: 32,
53 qk_dim: 64,
54 v_dim: 64,
55 patch_size: 32,
56 norm_eps: 5e-4,
57 layer_group_size: 48,
58 num_variate_layers_per_group: 1,
59 variate_layer_first: false,
60 use_xpos: true,
61 residual_mult: 0.75,
62 residual_attn_ratio: 5.136215466577748,
63 compute_f64: false,
64 }
65 }
66}
67
68impl InferConfig {
69 fn is_variate_layer(&self, idx: usize) -> bool {
70 if self.variate_layer_first {
71 idx % self.layer_group_size < self.num_variate_layers_per_group
72 } else {
73 idx % self.layer_group_size >= self.layer_group_size - self.num_variate_layers_per_group
74 }
75 }
76 fn q_size(&self) -> usize {
77 self.qk_dim * self.num_heads
78 }
79 fn k_size(&self) -> usize {
80 self.qk_dim * self.num_groups
81 }
82 fn v_size(&self) -> usize {
83 self.v_dim * self.num_groups
84 }
85
86 pub fn with_d_model(mut self, v: usize) -> Self {
89 self.d_model = v;
90 self
91 }
92 pub fn with_num_layers(mut self, v: usize) -> Self {
93 self.num_layers = v;
94 self
95 }
96 pub fn with_num_heads(mut self, v: usize) -> Self {
97 self.num_heads = v;
98 self
99 }
100 pub fn with_num_groups(mut self, v: usize) -> Self {
101 self.num_groups = v;
102 self
103 }
104 pub fn with_qk_dim(mut self, v: usize) -> Self {
105 self.qk_dim = v;
106 self
107 }
108 pub fn with_v_dim(mut self, v: usize) -> Self {
109 self.v_dim = v;
110 self
111 }
112 pub fn with_patch_size(mut self, v: usize) -> Self {
113 self.patch_size = v;
114 self
115 }
116 pub fn with_norm_eps(mut self, v: f64) -> Self {
117 self.norm_eps = v;
118 self
119 }
120 pub fn with_layer_group_size(mut self, v: usize) -> Self {
121 self.layer_group_size = v;
122 self
123 }
124 pub fn with_num_variate_layers_per_group(mut self, v: usize) -> Self {
125 self.num_variate_layers_per_group = v;
126 self
127 }
128 pub fn with_variate_layer_first(mut self, v: bool) -> Self {
129 self.variate_layer_first = v;
130 self
131 }
132 pub fn with_use_xpos(mut self, v: bool) -> Self {
133 self.use_xpos = v;
134 self
135 }
136 pub fn with_residual_mult(mut self, v: f64) -> Self {
137 self.residual_mult = v;
138 self
139 }
140 pub fn with_residual_attn_ratio(mut self, v: f64) -> Self {
141 self.residual_attn_ratio = v;
142 self
143 }
144
145 pub fn with_compute_f64(mut self, v: bool) -> Self {
148 self.compute_f64 = v;
149 self
150 }
151
152 pub fn patch_size(&self) -> usize {
155 self.patch_size
156 }
157 pub fn compute_f64(&self) -> bool {
158 self.compute_f64
159 }
160
161 pub fn from_json_value(v: &serde_json::Value) -> Self {
167 Self::default()
168 .with_d_model(json_usize(v, "d_model", 2048))
169 .with_num_layers(json_usize(v, "num_layers", 48))
170 .with_num_heads(json_usize(v, "num_heads", 32))
171 .with_num_groups(json_usize(v, "num_groups", 32))
172 .with_qk_dim(json_usize(v, "qk_dim", 64))
173 .with_v_dim(json_usize(v, "v_dim", 64))
174 .with_patch_size(json_usize(v, "patch_size", 32))
175 .with_norm_eps(json_f64(v, "norm_eps", 5e-4))
176 .with_layer_group_size(json_usize(v, "layer_group_size", 48))
177 .with_num_variate_layers_per_group(json_usize(v, "num_variate_layers_per_group", 1))
178 .with_variate_layer_first(json_bool(v, "variate_layer_first", false))
179 .with_use_xpos(json_bool(v, "use_xpos", true))
180 .with_residual_mult(json_f64(v, "residual_mult", 0.75))
181 .with_residual_attn_ratio(json_f64(v, "residual_attn_ratio", 5.136215466577748))
182 }
183}
184
185fn device() -> Device {
186 Device::flex()
187}
188
189fn load_t(weights: &GgufWeights, name: &str) -> Result<Tensor<2>> {
190 weights.load_2d(name)
191}
192
193fn load_v(weights: &GgufWeights, name: &str) -> Result<Tensor<1>> {
194 weights.load_1d(name)
195}
196
197fn try_load_v(weights: &GgufWeights, name: &str) -> Result<Option<Tensor<1>>> {
198 match weights.load_1d(name) {
199 Ok(t) => Ok(Some(t)),
200 Err(_) => Ok(None),
201 }
202}
203
204fn load_w(weights: &GgufWeights, name: &str, expected_d_out: usize) -> Result<Tensor<2>> {
205 weights.load_weight_2d(name, expected_d_out)
206}
207
208fn uu_linear<const D: usize>(x: Tensor<D>, w: Tensor<2>, b: Option<Tensor<1>>) -> Tensor<D> {
210 let d_in = w.dims()[1] as f32;
211 burn_linear_nd(x, w, b).mul_scalar(1.0 / d_in.sqrt())
212}
213
214fn uu_linear_readout<const D: usize>(
215 x: Tensor<D>,
216 w: Tensor<2>,
217 b: Option<Tensor<1>>,
218) -> Tensor<D> {
219 let d_in = w.dims()[1] as f32;
220 burn_linear_nd(x, w, b).mul_scalar(1.0 / d_in)
221}
222
223fn uu_silu<const D: usize>(x: Tensor<D>) -> Tensor<D> {
224 activation::silu(x).mul_scalar(SILU_SCALE)
225}
226
227fn rms_norm_weightless<const D: usize>(x: Tensor<D>, eps: f64) -> Tensor<D> {
228 let dims = x.dims();
229 let last = dims[D - 1];
230 let lead: usize = dims[..D - 1].iter().product();
231 let flat: Tensor<2> = x.reshape([lead, last]);
232 let mean_sq = flat.clone().square().mean_dim(1);
233 let rms = (mean_sq.add_scalar(eps as f32)).sqrt();
234 (flat / rms).reshape(dims)
235}
236
237fn residual_add<const D: usize>(h: Tensor<D>, skip: Tensor<D>, tau: f64) -> Tensor<D> {
238 let denom = (1.0 + tau * tau).sqrt() as f32;
239 h.mul_scalar(tau as f32 / denom) + skip.mul_scalar(1.0 / denom)
240}
241
242fn apply_per_dim_scale<const D: usize>(q: Tensor<D>, pds_w: Tensor<1>) -> Tensor<D> {
243 let sp = pds_w.exp() + 1.0;
245 let r = sp.log() / std::f32::consts::LN_2;
246 let dims = q.dims();
247 let last = dims[D - 1];
248 let lead: usize = dims[..D - 1].iter().product();
249 (q.reshape([lead, last]) * r.unsqueeze_dim::<2>(0)).reshape(dims)
250}
251
252struct BurnXposRope {
253 proj_width: usize,
254 cos: Vec<Vec<f32>>,
255 sin: Vec<Vec<f32>>,
256 xpos_base_scale: Vec<f32>,
257}
258
259impl BurnXposRope {
260 fn build(qk_dim: usize, max_len: usize) -> Self {
261 let proj_width = qk_dim / 2;
262 let half = proj_width / 2;
263 let base = 10000_f32;
264 let theta: Vec<f32> = (0..half)
265 .map(|i| 1.0 / base.powf(2.0 * i as f32 / proj_width as f32))
266 .collect();
267 let mut cos = vec![vec![0.0f32; proj_width]; max_len];
268 let mut sin = vec![vec![0.0f32; proj_width]; max_len];
269 for m in 0..max_len {
270 for i in 0..half {
271 let angle = m as f32 * theta[i];
272 cos[m][2 * i] = angle.cos();
273 cos[m][2 * i + 1] = angle.cos();
274 sin[m][2 * i] = angle.sin();
275 sin[m][2 * i + 1] = angle.sin();
276 }
277 }
278 let xpos_base_scale: Vec<f32> = (0..half)
279 .map(|i| ((2 * i) as f32 + 0.4 * proj_width as f32) / (1.4 * proj_width as f32))
280 .collect();
281 Self {
282 proj_width,
283 cos,
284 sin,
285 xpos_base_scale,
286 }
287 }
288
289 fn apply(&self, x: Tensor<4>, seq_ids: &[u32], xpos_exponent: f32) -> Tensor<4> {
290 let dims = x.dims();
291 let qk_dim = dims[3];
292 let proj_width = self.proj_width;
293 let half = proj_width / 2;
294 let seq_len = seq_ids.len();
295 let max_pos = seq_ids.iter().copied().max().unwrap_or(0) as f32;
296 let center = ((max_pos as u32 + 1) / 2) as f32;
297 let mut cos_data = vec![0.0f32; seq_len * proj_width];
298 let mut sin_data = vec![0.0f32; seq_len * proj_width];
299 for (si, &pos) in seq_ids.iter().enumerate() {
300 let power = (pos as f32 - center) / 256.0;
301 for i in 0..half {
302 let xpos_s = self.xpos_base_scale[i].powf(power).powf(xpos_exponent);
303 let c = self.cos[pos as usize][2 * i] * xpos_s;
304 let s = self.sin[pos as usize][2 * i] * xpos_s;
305 cos_data[si * proj_width + 2 * i] = c;
306 cos_data[si * proj_width + 2 * i + 1] = c;
307 sin_data[si * proj_width + 2 * i] = s;
308 sin_data[si * proj_width + 2 * i + 1] = s;
309 }
310 }
311 let dev = device();
312 let pos_cos =
313 Tensor::<4>::from_data(TensorData::new(cos_data, [1, 1, seq_len, proj_width]), &dev);
314 let pos_sin =
315 Tensor::<4>::from_data(TensorData::new(sin_data, [1, 1, seq_len, proj_width]), &dev);
316 let x_rot = x.clone().narrow(3, 0, proj_width);
318 let shp = x_rot.dims();
319 let pairs = x_rot.clone().reshape([shp[0], shp[1], shp[2], half, 2]);
320 let x1 = pairs.clone().narrow(4, 0, 1);
321 let x2 = pairs.narrow(4, 1, 1);
322 let rot_x = Tensor::cat(vec![x2.mul_scalar(-1.0), x1], 4)
323 .reshape([shp[0], shp[1], shp[2], proj_width]);
324 let rotated = x_rot * pos_cos + rot_x * pos_sin;
325 if qk_dim > proj_width {
326 let x_pass = x.narrow(3, proj_width, qk_dim - proj_width);
327 Tensor::cat(vec![rotated, x_pass], 3)
328 } else {
329 rotated
330 }
331 }
332}
333
334struct BurnResidualMlpW {
335 l1_w: Tensor<2>,
336 l1_b: Tensor<1>,
337 l2_w: Tensor<2>,
338 l2_b: Tensor<1>,
339 skip_w: Tensor<2>,
340 skip_b: Tensor<1>,
341 tau: f64,
342 is_output: bool,
343}
344
345struct BurnBlockW {
346 attn_qkv_w: Tensor<2>,
347 attn_qkv_b: Option<Tensor<1>>,
348 attn_out_w: Tensor<2>,
349 attn_out_b: Option<Tensor<1>>,
350 attn_pds: Option<Tensor<1>>,
351 attn_tau: f64,
352 ffn_up_w: Tensor<2>,
353 ffn_down_w: Tensor<2>,
354 mlp_tau: f64,
355}
356
357pub struct BurnTotoModel {
358 config: InferConfig,
359 rope: BurnXposRope,
360 patch_proj: BurnResidualMlpW,
361 blocks: Vec<BurnBlockW>,
362 output_head: BurnResidualMlpW,
363}
364
365fn load_residual_mlp(
366 weights: &GgufWeights,
367 prefix: &str,
368 tau: f64,
369 is_output: bool,
370) -> Result<BurnResidualMlpW> {
371 Ok(BurnResidualMlpW {
372 l1_w: load_t(weights, &format!("{prefix}.linear1.weight"))?,
373 l1_b: load_v(weights, &format!("{prefix}.linear1.bias"))?,
374 l2_w: load_t(weights, &format!("{prefix}.linear2.weight"))?,
375 l2_b: load_v(weights, &format!("{prefix}.linear2.bias"))?,
376 skip_w: load_t(weights, &format!("{prefix}.skip_proj.weight"))?,
377 skip_b: load_v(weights, &format!("{prefix}.skip_proj.bias"))?,
378 tau,
379 is_output,
380 })
381}
382
383fn compute_taus(
384 num_layers: usize,
385 residual_mult: f64,
386 residual_attn_ratio: f64,
387) -> (Vec<f64>, Vec<f64>) {
388 let total_depth = 2 * num_layers;
389 let alpha_mlp = residual_mult * (2.0 / (1.0 + residual_attn_ratio.powi(2))).sqrt();
390 let alpha_attn = residual_attn_ratio * alpha_mlp;
391 let tau = |index: usize| -> f64 {
392 let n_attn = (index + 1) / 2;
393 let n_mlp = index / 2;
394 let num = if index % 2 == 0 {
395 alpha_attn
396 } else {
397 alpha_mlp
398 };
399 let den = (total_depth as f64 / 2.0
400 + n_attn as f64 * alpha_attn.powi(2)
401 + n_mlp as f64 * alpha_mlp.powi(2))
402 .sqrt();
403 num / den
404 };
405 (0..num_layers)
406 .map(|i| (tau(2 * i), tau(2 * i + 1)))
407 .unzip()
408}
409
410impl BurnTotoModel {
411 pub fn patch_size(&self) -> usize {
412 self.config.patch_size()
413 }
414
415 pub fn load(gguf_path: &Path, config: InferConfig) -> Result<Self> {
416 if config.compute_f64 {
417 bail!("BurnTotoModel supports compute_f64=false only");
418 }
419 let weights = open_gguf_weights(gguf_path)
420 .with_context(|| format!("open {}", gguf_path.display()))?;
421
422 let patch_proj = load_residual_mlp(&weights, "patch_proj", 1.0, false)?;
423
424 let (attn_taus, mlp_taus) = compute_taus(
425 config.num_layers,
426 config.residual_mult,
427 config.residual_attn_ratio,
428 );
429 let mut blocks = Vec::with_capacity(config.num_layers);
430 for n in 0..config.num_layers {
431 blocks.push(BurnBlockW {
432 attn_qkv_w: load_t(&weights, &format!("blk.{n}.attn_qkv.weight"))?,
433 attn_qkv_b: try_load_v(&weights, &format!("blk.{n}.attn_qkv.bias"))?,
434 attn_out_w: load_t(&weights, &format!("blk.{n}.attn_output.weight"))?,
435 attn_out_b: try_load_v(&weights, &format!("blk.{n}.attn_output.bias"))?,
436 attn_pds: try_load_v(&weights, &format!("blk.{n}.attn_pds.weight"))?,
437 attn_tau: attn_taus[n],
438 ffn_up_w: load_t(&weights, &format!("blk.{n}.ffn_up.weight"))?,
439 ffn_down_w: load_w(
441 &weights,
442 &format!("blk.{n}.ffn_down.weight"),
443 config.d_model,
444 )?,
445 mlp_tau: mlp_taus[n],
446 });
447 }
448 let output_head = load_residual_mlp(&weights, "output_head", 1.0, true)?;
449 let rope = BurnXposRope::build(config.qk_dim, 8192);
450 Ok(Self {
451 config,
452 rope,
453 patch_proj,
454 blocks,
455 output_head,
456 })
457 }
458
459 pub fn forecast(
460 &self,
461 target: &[Vec<f32>],
462 mask: &[Vec<bool>],
463 prediction_length: usize,
464 ) -> Result<Vec<Vec<Vec<f32>>>> {
465 let n_var = target.len();
466 let ctx_len = target[0].len();
467 let patch_size = self.config.patch_size;
468 if ctx_len % patch_size != 0 {
469 bail!("ctx_len ({ctx_len}) must be divisible by patch_size ({patch_size})");
470 }
471 let ctx_patches = ctx_len / patch_size;
472 let fcst_patches = (prediction_length + patch_size - 1) / patch_size;
473 let total_patches = ctx_patches + fcst_patches;
474
475 let mut locs = Vec::with_capacity(n_var);
476 let mut scales = Vec::with_capacity(n_var);
477 for v in 0..n_var {
478 let (loc, scale) = causal_patched_std_scaler(&target[v], &mask[v], patch_size);
479 locs.push(loc);
480 scales.push(scale);
481 }
482
483 let mut patch_data = vec![0.0f32; n_var * total_patches * 2 * patch_size];
484 for v in 0..n_var {
485 for p in 0..ctx_patches {
486 let base = v * total_patches * 2 * patch_size + p * 2 * patch_size;
487 for i in 0..patch_size {
488 let t = p * patch_size + i;
489 let obs = mask[v][t];
490 let val = if obs {
491 (((target[v][t] - locs[v][t]) / scales[v][t]) as f64).asinh() as f32
492 } else {
493 0.0
494 };
495 patch_data[base + i] = val;
496 patch_data[base + patch_size + i] = if obs { 0.0 } else { 1.0 };
497 }
498 }
499 for p in ctx_patches..total_patches {
500 let base = v * total_patches * 2 * patch_size + p * 2 * patch_size;
501 for i in 0..patch_size {
502 patch_data[base + i] = 0.0;
503 patch_data[base + patch_size + i] = 1.0;
504 }
505 }
506 }
507
508 let dev = device();
509 let x = Tensor::<4>::from_data(
510 TensorData::new(patch_data, [1, n_var, total_patches, 2 * patch_size]),
511 &dev,
512 );
513 let x = self.forward_residual_mlp(x, &self.patch_proj)?;
514 let x = self.forward_transformer(x, n_var, total_patches)?;
515 let x = self.forward_residual_mlp(x, &self.output_head)?;
516 let x: Tensor<4> = x.narrow(2, ctx_patches - 1, fcst_patches);
517 let fcst_steps = fcst_patches * patch_size;
518 let out: Tensor<5> = x.reshape([1, n_var, fcst_patches, patch_size, 9]);
519 let out: Tensor<5> = out.permute([4, 0, 1, 2, 3]);
520 let out: Tensor<3> = out.reshape([9, n_var, fcst_steps]);
521 let h = out.dims()[2];
522 let out: Tensor<3> = if fcst_steps > prediction_length {
523 out.narrow(2, 0, prediction_length)
524 } else {
525 out
526 };
527 let out_data: Vec<f32> = out
528 .to_data()
529 .try_to_vec()
530 .map_err(|e| anyhow::anyhow!("{e:?}"))?;
531
532 let loc_final: Vec<f32> = (0..n_var).map(|v| locs[v][ctx_len - 1]).collect();
533 let scale_final: Vec<f32> = (0..n_var).map(|v| scales[v][ctx_len - 1]).collect();
534 let mut result = vec![vec![vec![0.0f32; prediction_length]; n_var]; 9];
535 for q in 0..9 {
536 for v in 0..n_var {
537 for t in 0..prediction_length {
538 let raw = out_data[(q * n_var + v) * h + t] as f64;
539 result[q][v][t] = (raw.sinh() as f32) * scale_final[v] + loc_final[v];
540 }
541 }
542 }
543 Ok(result)
544 }
545
546 fn forward_residual_mlp<const D: usize>(
547 &self,
548 x: Tensor<D>,
549 w: &BurnResidualMlpW,
550 ) -> Result<Tensor<D>> {
551 let h = uu_silu(uu_linear(x.clone(), w.l1_w.clone(), Some(w.l1_b.clone())));
552 let h = if w.is_output {
553 uu_linear_readout(h, w.l2_w.clone(), Some(w.l2_b.clone()))
554 } else {
555 uu_linear(h, w.l2_w.clone(), Some(w.l2_b.clone()))
556 };
557 let skip = if w.is_output {
558 uu_linear_readout(x, w.skip_w.clone(), Some(w.skip_b.clone()))
559 } else {
560 uu_linear(x, w.skip_w.clone(), Some(w.skip_b.clone()))
561 };
562 Ok(residual_add(h, skip, w.tau))
563 }
564
565 fn forward_transformer(
566 &self,
567 mut x: Tensor<4>,
568 n_var: usize,
569 num_patches: usize,
570 ) -> Result<Tensor<4>> {
571 for (idx, blk) in self.blocks.iter().enumerate() {
572 x = if self.config.is_variate_layer(idx) {
573 self.forward_variate_layer(x, blk, n_var, num_patches)?
574 } else {
575 self.forward_time_layer(x, blk, n_var, num_patches)?
576 };
577 }
578 Ok(rms_norm_weightless(x, self.config.norm_eps))
579 }
580
581 fn forward_time_layer(
582 &self,
583 x: Tensor<4>,
584 blk: &BurnBlockW,
585 n_var: usize,
586 num_patches: usize,
587 ) -> Result<Tensor<4>> {
588 let cfg = &self.config;
589 let state: Tensor<3> = x.reshape([n_var, num_patches, cfg.d_model]);
590 let normed = rms_norm_weightless(state.clone(), cfg.norm_eps);
591 let seq_ids: Vec<u32> = (0..num_patches as u32).collect();
592 let attn_out = self.forward_attention(&normed, blk, &seq_ids, false)?;
593 let state = residual_add(attn_out, state, blk.attn_tau);
594 let normed = rms_norm_weightless(state.clone(), cfg.norm_eps);
595 let ffn_out = self.forward_ffn(&normed, blk)?;
596 let state = residual_add(ffn_out, state, blk.mlp_tau);
597 Ok(state.reshape([1, n_var, num_patches, cfg.d_model]))
598 }
599
600 fn forward_variate_layer(
601 &self,
602 x: Tensor<4>,
603 blk: &BurnBlockW,
604 n_var: usize,
605 num_patches: usize,
606 ) -> Result<Tensor<4>> {
607 let cfg = &self.config;
608 let state: Tensor<3> = x
609 .permute([0, 2, 1, 3])
610 .reshape([num_patches, n_var, cfg.d_model]);
611 let normed = rms_norm_weightless(state.clone(), cfg.norm_eps);
612 let attn_out = self.forward_attention(&normed, blk, &[], true)?;
613 let state = residual_add(attn_out, state, blk.attn_tau);
614 let normed = rms_norm_weightless(state.clone(), cfg.norm_eps);
615 let ffn_out = self.forward_ffn(&normed, blk)?;
616 let state = residual_add(ffn_out, state, blk.mlp_tau);
617 Ok(state
618 .reshape([1, num_patches, n_var, cfg.d_model])
619 .permute([0, 2, 1, 3]))
620 }
621
622 fn forward_attention(
623 &self,
624 state: &Tensor<3>,
625 blk: &BurnBlockW,
626 seq_ids: &[u32],
627 is_variate: bool,
628 ) -> Result<Tensor<3>> {
629 let cfg = &self.config;
630 let dims = state.dims();
631 let (batch, seq) = (dims[0], dims[1]);
632 let qkv = uu_linear(
633 state.clone(),
634 blk.attn_qkv_w.clone(),
635 blk.attn_qkv_b.clone(),
636 );
637 let q: Tensor<3> = qkv.clone().narrow(2, 0, cfg.q_size());
638 let k: Tensor<3> = qkv.clone().narrow(2, cfg.q_size(), cfg.k_size());
639 let v: Tensor<3> = qkv.narrow(2, cfg.q_size() + cfg.k_size(), cfg.v_size());
640 let pack = |t: Tensor<3>, heads: usize, hd: usize| {
641 t.reshape([batch, seq, heads, hd]).permute([0, 2, 1, 3])
642 };
643 let (q, k, v) = (
644 pack(q, cfg.num_heads, cfg.qk_dim),
645 pack(k, cfg.num_groups, cfg.qk_dim),
646 pack(v, cfg.num_groups, cfg.v_dim),
647 );
648 let q = match blk.attn_pds.as_ref() {
649 Some(pds) => apply_per_dim_scale(q, pds.clone()),
650 None => q,
651 };
652 let (q, k) = if !is_variate && !seq_ids.is_empty() && cfg.use_xpos {
653 (
654 self.rope.apply(q, seq_ids, 1.0),
655 self.rope.apply(k, seq_ids, -1.0),
656 )
657 } else {
658 (q, k)
659 };
660 let scale = 1.0f32 / cfg.qk_dim as f32;
661 let scores = q.matmul(k.transpose()).mul_scalar(scale);
662 let scores = if !is_variate {
663 scores + burn_causal_mask(seq)
664 } else {
665 scores
666 };
667 let attn = activation::softmax(scores, 3);
668 let out = attn.matmul(v);
669 let out: Tensor<3> =
670 out.permute([0, 2, 1, 3])
671 .reshape([batch, seq, cfg.num_heads * cfg.v_dim]);
672 Ok(uu_linear(
673 out,
674 blk.attn_out_w.clone(),
675 blk.attn_out_b.clone(),
676 ))
677 }
678
679 fn forward_ffn(&self, x: &Tensor<3>, blk: &BurnBlockW) -> Result<Tensor<3>> {
680 let fc1_out = uu_linear(x.clone(), blk.ffn_up_w.clone(), None);
681 let half = fc1_out.dims()[2] / 2;
682 let gate = fc1_out.clone().narrow(2, 0, half);
683 let val = fc1_out.narrow(2, half, half);
684 let activated = gate * activation::silu(val);
685 Ok(uu_linear(activated, blk.ffn_down_w.clone(), None))
686 }
687}
688
689fn burn_causal_mask(seq: usize) -> Tensor<4> {
690 let dev = device();
691 let mut data = vec![0.0f32; seq * seq];
692 for i in 0..seq {
693 for j in (i + 1)..seq {
694 data[i * seq + j] = f32::NEG_INFINITY;
695 }
696 }
697 Tensor::<2>::from_data(TensorData::new(data, [seq, seq]), &dev)
698 .unsqueeze_dim::<3>(0)
699 .unsqueeze_dim::<4>(0)
700}
701
702fn causal_patched_std_scaler(
703 data: &[f32],
704 mask: &[bool],
705 patch_size: usize,
706) -> (Vec<f32>, Vec<f32>) {
707 let n = data.len();
708 let num_patches = n / patch_size;
709 let mut cum_count = 0.0f64;
710 let mut m1 = 0.0f64;
711 let mut m2 = 0.0f64;
712 let correction = 1.0f64;
713 let minimum_scale = 1e-6f64;
714 let mut patch_loc = vec![0.0f32; num_patches];
715 let mut patch_scale = vec![1e-6f32; num_patches];
716 for p in 0..num_patches {
717 for i in 0..patch_size {
718 let t = p * patch_size + i;
719 if mask[t] {
720 let x = data[t] as f64;
721 cum_count += 1.0;
722 let prev_m1 = m1;
723 m1 += (x - m1) / cum_count;
724 m2 += (x - prev_m1) * (x - m1);
725 }
726 }
727 patch_loc[p] = m1 as f32;
728 let denom = (cum_count - correction).max(1.0);
729 patch_scale[p] = (m2 / denom).sqrt().max(minimum_scale) as f32;
730 }
731 let mut loc = vec![0.0f32; n];
732 let mut scale = vec![1e-6f32; n];
733 for p in 0..num_patches {
734 for i in 0..patch_size {
735 let t = p * patch_size + i;
736 loc[t] = patch_loc[p];
737 scale[t] = patch_scale[p];
738 }
739 }
740 (loc, scale)
741}
742
743#[cfg(test)]
744mod tests {
745 use super::*;
746 use zsfm_gguf::{GGMLType, GGUFMetaValue, GGUFWriter};
747
748 fn pseudo(n: usize, seed: u64) -> Vec<f32> {
749 let mut x = seed;
750 (0..n)
751 .map(|_| {
752 x = x
753 .wrapping_mul(6364136223846793005)
754 .wrapping_add(1442695040888963407);
755 ((x >> 33) as f32 / u32::MAX as f32) * 2.0 - 1.0
756 })
757 .collect()
758 }
759
760 fn tiny_config() -> InferConfig {
761 InferConfig::default()
762 .with_d_model(16)
763 .with_num_layers(1)
764 .with_num_heads(4)
765 .with_num_groups(4)
766 .with_qk_dim(8)
767 .with_v_dim(8)
768 .with_patch_size(4)
769 }
770
771 fn put2(w: &mut GGUFWriter, name: &str, rows: usize, cols: usize, seed: &mut u64, scale: f32) {
772 *seed += 1;
773 let data: Vec<f32> = pseudo(rows * cols, *seed)
774 .iter()
775 .map(|v| v * scale)
776 .collect();
777 let bytes: Vec<u8> = data.iter().flat_map(|v| v.to_le_bytes()).collect();
778 w.add_tensor(name, vec![cols as u64, rows as u64], GGMLType::F32, bytes);
779 }
780
781 fn put1(w: &mut GGUFWriter, name: &str, n: usize, seed: &mut u64) {
782 *seed += 1;
783 let bytes: Vec<u8> = pseudo(n, *seed)
784 .iter()
785 .flat_map(|v| v.to_le_bytes())
786 .collect();
787 w.add_tensor(name, vec![n as u64], GGMLType::F32, bytes);
788 }
789
790 fn put_mlp(
791 w: &mut GGUFWriter,
792 prefix: &str,
793 h1: usize,
794 d_in: usize,
795 h2: usize,
796 seed: &mut u64,
797 s: f32,
798 ) {
799 put2(w, &format!("{prefix}.linear1.weight"), h1, d_in, seed, s);
800 put1(w, &format!("{prefix}.linear1.bias"), h1, seed);
801 put2(w, &format!("{prefix}.linear2.weight"), h2, h1, seed, s);
802 put1(w, &format!("{prefix}.linear2.bias"), h2, seed);
803 put2(w, &format!("{prefix}.skip_proj.weight"), h2, d_in, seed, s);
804 put1(w, &format!("{prefix}.skip_proj.bias"), h2, seed);
805 }
806
807 #[test]
808 fn burn_forecast_golden() {
809 let dir = std::env::temp_dir().join(format!("toto-burn-{}", std::process::id()));
811 let _ = std::fs::remove_dir_all(&dir);
812 std::fs::create_dir_all(&dir).unwrap();
813 let path = dir.join("model.gguf");
814 let mut w = GGUFWriter::new();
815 w.add_metadata("general.architecture", GGUFMetaValue::String("toto".into()));
816 let mut seed = 1100u64;
817 let s = 0.05;
818 put_mlp(&mut w, "patch_proj", 32, 8, 16, &mut seed, s);
819 put2(&mut w, "blk.0.attn_qkv.weight", 96, 16, &mut seed, s);
820 put1(&mut w, "blk.0.attn_qkv.bias", 96, &mut seed);
821 put2(&mut w, "blk.0.attn_output.weight", 16, 32, &mut seed, s);
822 put1(&mut w, "blk.0.attn_output.bias", 16, &mut seed);
823 put1(&mut w, "blk.0.attn_pds.weight", 8, &mut seed);
824 put2(&mut w, "blk.0.ffn_up.weight", 32, 16, &mut seed, s);
825 put2(&mut w, "blk.0.ffn_down.weight", 16, 16, &mut seed, s);
826 put_mlp(&mut w, "output_head", 32, 16, 36, &mut seed, s);
827 let f = std::fs::File::create(&path).unwrap();
828 w.write_to(&mut std::io::BufWriter::new(f)).unwrap();
829
830 let cfg = tiny_config();
831 assert!(!cfg.compute_f64);
832 let burn = BurnTotoModel::load(&path, cfg).unwrap();
833 let ctx = vec![(0..8).map(|i| 10.0 + 0.5 * i as f32).collect::<Vec<_>>()];
834 let mask = vec![vec![true; 8]];
835 let t0 = std::time::Instant::now();
836 let a = burn.forecast(&ctx, &mask, 4).unwrap();
837 let burn_ms = t0.elapsed().as_secs_f64() * 1000.0;
838 let b = burn.forecast(&ctx, &mask, 4).unwrap();
839 assert_eq!(a.len(), 9);
840 for q in &a {
841 assert_eq!(q.len(), 1);
842 for v in q {
843 assert_eq!(v.len(), 4);
844 }
845 }
846 assert!(a.iter().flatten().flatten().all(|v| v.is_finite()));
847 assert!(a.iter().flatten().flatten().any(|&v| v != 0.0));
848 assert_eq!(a, b, "forecast must be deterministic");
849 println!("toto synthetic: burn {burn_ms:.1}ms out={a:?}");
850 let flat: Vec<f32> = a.iter().flatten().flatten().copied().collect();
852 let golden: [f32; 36] = [
853 11.704744, 11.739721, 11.7525215, 11.725984, 11.722208, 11.724002, 11.743584,
854 11.738862, 11.74826, 11.730129, 11.741938, 11.736687, 11.761163, 11.724331, 11.729776,
855 11.752375, 11.711724, 11.757467, 11.735854, 11.761217, 11.6961, 11.734921, 11.741099,
856 11.715156, 11.7233715, 11.694718, 11.721759, 11.7119465, 11.716567, 11.721282,
857 11.730694, 11.710778, 11.740484, 11.723714, 11.738366, 11.721424,
858 ];
859 assert_eq!(flat.len(), golden.len());
860 for (x, y) in flat.iter().zip(golden.iter()) {
861 assert!((x - y).abs() < 1e-3, "golden drift: {x} vs {y}");
862 }
863 let _ = std::fs::remove_dir_all(&dir);
864 }
865}