1use std::io::{BufReader, Read, Seek};
10use std::path::Path;
11
12use anyhow::{Context, Result};
13use candle_core::quantized::gguf_file;
14use candle_core::{DType, Device, Tensor, D};
15
16use std::collections::HashMap;
17use std::sync::Mutex;
18
19use rayon::prelude::*;
20
21use crate::config::MoiraiConfig;
22
23const PATCH_SIZE: usize = 32;
25const PATCH_IDX: usize = 2;
26
27struct EncoderBlock {
32 norm1_w: Tensor,
33 norm2_w: Tensor,
34 attn_qkv_w: Tensor, attn_o_w: Tensor,
36 attn_qn_w: Tensor, attn_kn_w: Tensor, vbias_obs: Tensor, vbias_mask: Tensor, ffn_fc1_w: Tensor,
41 ffn_fc2_w: Tensor,
42 ffn_gate_w: Tensor,
43}
44
45pub struct MoiraiModel {
46 device: Device,
47 config: MoiraiConfig,
48 in_proj_w: Tensor, in_proj_b: Tensor, mask_embed: Tensor, blocks: Vec<EncoderBlock>,
52 norm_f_w: Tensor,
53 head_st_loc_w: Tensor, head_st_loc_b: Tensor, rope_inv_freq: Vec<f32>,
56 rope_cache: Mutex<HashMap<usize, (Tensor, Tensor)>>,
57}
58
59fn load_t(
64 content: &gguf_file::Content,
65 reader: &mut (impl Read + Seek),
66 name: &str,
67 device: &Device,
68) -> Result<Tensor> {
69 zsfm_nn::load_tensor(content, reader, name, device, DType::F32)
70}
71
72impl MoiraiModel {
73 pub fn load(gguf_path: &Path, config: MoiraiConfig) -> Result<Self> {
74 let device = Device::Cpu;
75 let file = std::fs::File::open(gguf_path)
76 .with_context(|| format!("open {}", gguf_path.display()))?;
77 let mut reader = BufReader::with_capacity(zsfm_gguf::READ_BUF_CAPACITY, file);
78 let content = gguf_file::Content::read(&mut reader).context("parse GGUF header")?;
79
80 let in_proj_w = load_t(&content, &mut reader, "in_proj.weight", &device)?;
81 let in_proj_b = load_t(&content, &mut reader, "in_proj.bias", &device)?;
82 let mask_embed = load_t(&content, &mut reader, "mask_embed.weight", &device)?;
83
84 let mut blocks = Vec::with_capacity(config.n_layers);
85 for n in 0..config.n_layers {
86 let p = |s: &str| format!("blk.{n}.{s}");
87 let q_w = load_t(&content, &mut reader, &p("attn_q.weight"), &device)?;
88 let k_w = load_t(&content, &mut reader, &p("attn_k.weight"), &device)?;
89 let v_w = load_t(&content, &mut reader, &p("attn_v.weight"), &device)?;
90 let attn_qkv_w = Tensor::cat(&[&q_w, &k_w, &v_w], 0)
91 .with_context(|| format!("qkv cat blk.{n}"))?;
92 let norm1_w = load_t(&content, &mut reader, &p("norm1.weight"), &device)?;
93 let norm2_w = load_t(&content, &mut reader, &p("norm2.weight"), &device)?;
94 let attn_o_w = load_t(&content, &mut reader, &p("attn_o.weight"), &device)?;
95 let attn_qn_w = load_t(&content, &mut reader, &p("attn_qn.weight"), &device)?;
96 let attn_kn_w = load_t(&content, &mut reader, &p("attn_kn.weight"), &device)?;
97 let vbias_raw = load_t(&content, &mut reader, &p("attn_vbias.weight"), &device)?
98 .flatten_all()?.to_vec1::<f32>()?;
99 let n_heads = config.n_heads;
100 let vbias_obs = Tensor::from_vec(vbias_raw[0..n_heads].to_vec(), (n_heads, 1, 1), &device)?;
101 let vbias_mask = Tensor::from_vec(vbias_raw[n_heads..2*n_heads].to_vec(), (n_heads, 1, 1), &device)?;
102 let ffn_fc1_w = load_t(&content, &mut reader, &p("ffn_fc1.weight"), &device)?;
103 let ffn_fc2_w = load_t(&content, &mut reader, &p("ffn_fc2.weight"), &device)?;
104 let ffn_gate_w = load_t(&content, &mut reader, &p("ffn_gate.weight"), &device)?;
105 blocks.push(EncoderBlock {
106 norm1_w, norm2_w, attn_qkv_w, attn_o_w, attn_qn_w, attn_kn_w,
107 vbias_obs, vbias_mask, ffn_fc1_w, ffn_fc2_w, ffn_gate_w,
108 });
109 }
110
111 let norm_f_w = load_t(&content, &mut reader, "norm_f.weight", &device)?;
112 let head_st_loc_w = load_t(&content, &mut reader, "head.st_loc.weight", &device)?;
113 let head_st_loc_b = load_t(&content, &mut reader, "head.st_loc.bias", &device)?;
114
115 let head_dim = config.head_dim;
116 let half = head_dim / 2;
117 let rope_inv_freq: Vec<f32> = (0..half)
118 .map(|i| 1.0_f32 / 10000_f32.powf(2.0 * i as f32 / head_dim as f32))
119 .collect();
120
121 Ok(Self {
122 device, config,
123 in_proj_w, in_proj_b, mask_embed,
124 blocks, norm_f_w,
125 head_st_loc_w, head_st_loc_b,
126 rope_inv_freq,
127 rope_cache: Mutex::new(HashMap::new()),
128 })
129 }
130
131 pub fn forecast(&self, context: &[f32], horizon: usize) -> Result<Vec<f32>> {
137 let cfg = &self.config;
138 let patch_size = PATCH_SIZE;
139 let patch_idx = PATCH_IDX;
140
141 let loc = context.iter().map(|&v| v as f64).sum::<f64>() / context.len() as f64;
143 let scale = context.iter().map(|&v| (v as f64 - loc).abs()).sum::<f64>()
144 / context.len() as f64;
145 let scale = (scale.max(1e-8)) as f32;
146 let loc = loc as f32;
147
148 let max_ts = cfg.max_seq_len; let mut ctx_scaled: Vec<f32> = context.iter().map(|&v| (v - loc) / scale).collect();
151 if ctx_scaled.len() > max_ts {
152 let start = ctx_scaled.len() - max_ts;
153 ctx_scaled = ctx_scaled[start..].to_vec();
154 }
155 let ctx_len = ctx_scaled.len();
157 let ctx_padded_len = ((ctx_len + patch_size - 1) / patch_size) * patch_size;
158 if ctx_padded_len > ctx_len {
159 let mut padded = vec![0.0f32; ctx_padded_len - ctx_len];
160 padded.extend_from_slice(&ctx_scaled);
161 ctx_scaled = padded;
162 }
163 let n_ctx_patches = ctx_scaled.len() / patch_size;
164
165 let n_fc_patches = (horizon + patch_size - 1) / patch_size;
167 let total_patches = n_ctx_patches + n_fc_patches;
168
169 let d_model = cfg.d_model; let max_ps = cfg.max_patch_size; let in_proj_w_3d = self.in_proj_w.reshape((5, d_model, max_ps))?;
182 let proj_w_slice = in_proj_w_3d.get(patch_idx)?.contiguous()?; let proj_w = proj_w_slice.narrow(1, 0, patch_size)?.contiguous()?;
186
187 let in_proj_b_2d = self.in_proj_b.reshape((5, d_model))?;
189 let proj_b = in_proj_b_2d.get(patch_idx)?.contiguous()?; let mut patch_flat = vec![0.0f32; n_ctx_patches * patch_size];
193 for i in 0..n_ctx_patches {
194 let src = &ctx_scaled[i * patch_size..(i + 1) * patch_size];
195 patch_flat[i * patch_size..(i + 1) * patch_size].copy_from_slice(src);
196 }
197 let ctx_patches_t = Tensor::from_vec(
198 patch_flat, (n_ctx_patches, patch_size), &self.device,
199 )?;
200 let ctx_emb = ctx_patches_t
202 .matmul(&proj_w.t()?)?
203 .broadcast_add(&proj_b)?;
204
205 let mask_embed = self.mask_embed.reshape((1, d_model))?;
208 let fc_emb = mask_embed.expand((n_fc_patches, d_model))?;
209
210 let mut h = Tensor::cat(&[&ctx_emb, &fc_emb], 0)?;
212
213 let mut is_masked = vec![0u8; total_patches];
216 for i in n_ctx_patches..total_patches {
217 is_masked[i] = 1;
218 }
219
220 for blk in &self.blocks {
222 h = self.forward_block(&h, blk, &is_masked, total_patches)?;
223 }
224 h = zsfm_nn::rms_norm(&h, Some(&self.norm_f_w), 1e-6)?;
225
226 let loc_w_3d = self.head_st_loc_w.reshape((5, max_ps, d_model))?;
230 let loc_b_2d = self.head_st_loc_b.reshape((5, max_ps))?;
231 let loc_w = loc_w_3d.get(patch_idx)?.narrow(0, 0, patch_size)?.contiguous()?; let loc_b = loc_b_2d.get(patch_idx)?.narrow(0, 0, patch_size)?.contiguous()?; let future_h = h.narrow(0, n_ctx_patches, n_fc_patches)?; let pred = future_h.matmul(&loc_w.t()?)?.broadcast_add(&loc_b)?;
237
238 let pred_flat: Vec<f32> = pred.flatten_all()?.to_vec1()?;
239 let result: Vec<f32> = pred_flat
240 .iter()
241 .take(horizon)
242 .map(|&v| v * scale + loc)
243 .collect();
244
245 Ok(result)
246 }
247
248 fn forward_block(
249 &self,
250 hidden: &Tensor,
251 blk: &EncoderBlock,
252 is_masked: &[u8],
253 seq_len: usize,
254 ) -> Result<Tensor> {
255 let res = hidden;
256 let h = zsfm_nn::rms_norm(hidden, Some(&blk.norm1_w), 1e-6)?;
257 let h = self.qk_attn(&h, blk, is_masked, seq_len)?;
258 let h = (h + res)?;
259
260 let res2 = h.clone();
261 let h2 = zsfm_nn::rms_norm(&h, Some(&blk.norm2_w), 1e-6)?;
262 let h2 = zsfm_nn::swiglu_ffn(&h2, &blk.ffn_fc1_w, &blk.ffn_fc2_w, &blk.ffn_gate_w)?;
263 Ok((h2 + res2)?)
264 }
265
266 fn qk_attn(
267 &self,
268 hidden: &Tensor,
269 blk: &EncoderBlock,
270 is_masked: &[u8],
271 seq_len: usize,
272 ) -> Result<Tensor> {
273 let cfg = &self.config;
274 let n_heads = cfg.n_heads;
275 let head_dim = cfg.head_dim;
276 let d_model = cfg.d_model;
277
278 let qkv = zsfm_nn::linear_nobias(hidden, &blk.attn_qkv_w)?;
279 let q = qkv.narrow(D::Minus1, 0, d_model)?;
280 let k = qkv.narrow(D::Minus1, d_model, d_model)?;
281 let v = qkv.narrow(D::Minus1, 2 * d_model, d_model)?;
282
283 let q = q.reshape((seq_len, n_heads, head_dim))?;
285 let k = k.reshape((seq_len, n_heads, head_dim))?;
286
287 let q = qk_norm_heads(&q, &blk.attn_qn_w, seq_len, n_heads, head_dim)?;
289 let k = qk_norm_heads(&k, &blk.attn_kn_w, seq_len, n_heads, head_dim)?;
290
291 let q = q.permute((1, 0, 2))?.contiguous()?;
293 let k = k.permute((1, 0, 2))?.contiguous()?;
294
295 let (cos_t, sin_t) = {
297 let mut cache = self.rope_cache.lock().unwrap();
298 if !cache.contains_key(&seq_len) {
299 let half = self.rope_inv_freq.len();
300 let mut cos_v = vec![0.0f32; seq_len * half];
301 let mut sin_v = vec![0.0f32; seq_len * half];
302 for pos in 0..seq_len {
303 for i in 0..half {
304 let theta = pos as f32 * self.rope_inv_freq[i];
305 cos_v[pos * half + i] = theta.cos();
306 sin_v[pos * half + i] = theta.sin();
307 }
308 }
309 let half_dim = self.rope_inv_freq.len();
310 let cos_t = Tensor::from_vec(cos_v, (seq_len, half_dim), &self.device)?.unsqueeze(0)?;
311 let sin_t = Tensor::from_vec(sin_v, (seq_len, half_dim), &self.device)?.unsqueeze(0)?;
312 cache.insert(seq_len, (cos_t, sin_t));
313 }
314 let (c, s) = &cache[&seq_len];
315 (c.clone(), s.clone())
316 };
317
318 let q = apply_rope_with_tables(&q, &cos_t, &sin_t, head_dim)?;
319 let k = apply_rope_with_tables(&k, &cos_t, &sin_t, head_dim)?;
320 let v = v.reshape((seq_len, n_heads, head_dim))?.permute((1, 0, 2))?.contiguous()?;
321
322 let scale = (head_dim as f64).sqrt();
323 let scores = q.matmul(&k.permute((0, 2, 1))?)?; let scores = (scores / scale)?;
325
326 let scores = apply_var_attn_bias(&scores, is_masked, seq_len, &blk.vbias_obs, &blk.vbias_mask, &self.device)?;
328
329 let attn = candle_nn::ops::softmax_last_dim(&scores)?;
330 let out = attn.matmul(&v)?; let out = out.permute((1, 0, 2))?.contiguous()?.reshape((seq_len, d_model))?;
332
333 zsfm_nn::linear_nobias(&out, &blk.attn_o_w)
334 }
335}
336
337fn qk_norm_heads(
344 x: &Tensor,
345 weight: &Tensor, seq_len: usize,
347 n_heads: usize,
348 head_dim: usize,
349) -> Result<Tensor> {
350 let x_flat = x.reshape((seq_len * n_heads, head_dim))?;
351 let normed = zsfm_nn::rms_norm(&x_flat, Some(weight), 1e-6)?;
352 Ok(normed.reshape((seq_len, n_heads, head_dim))?)
353}
354
355fn apply_var_attn_bias(
360 scores: &Tensor,
361 is_masked: &[u8],
362 seq_len: usize,
363 vbias_obs: &Tensor, vbias_mask: &Tensor, device: &Device,
366) -> Result<Tensor> {
367 let mask_f: Vec<f32> = is_masked.iter().map(|&m| m as f32).collect();
368 let mask_t = Tensor::from_vec(mask_f, (1usize, 1, seq_len), device)?;
369 let delta = (vbias_mask - vbias_obs)?;
370 let bias = vbias_obs.broadcast_add(&delta.broadcast_mul(&mask_t)?)?;
371 Ok(scores.broadcast_add(&bias)?)
372}
373
374fn apply_rope_with_tables(
377 x: &Tensor,
378 cos_t: &Tensor,
379 sin_t: &Tensor,
380 head_dim: usize,
381) -> Result<Tensor> {
382 let half = head_dim / 2;
383 let x1 = x.narrow(D::Minus1, 0, half)?.contiguous()?;
384 let x2 = x.narrow(D::Minus1, half, half)?.contiguous()?;
385 let rot1 = (x1.broadcast_mul(cos_t)? - x2.broadcast_mul(sin_t)?)?;
386 let rot2 = (x1.broadcast_mul(sin_t)? + x2.broadcast_mul(cos_t)?)?;
387 Ok(Tensor::cat(&[&rot1, &rot2], D::Minus1)?.contiguous()?)
388}
389
390impl zsfm_core::Forecaster for MoiraiModel {
395 type Config = MoiraiConfig;
396
397 fn load(gguf_path: &Path, config: MoiraiConfig) -> Result<Self> {
398 MoiraiModel::load(gguf_path, config)
399 }
400
401 fn forecast(
407 &self,
408 context: &[Vec<f32>],
409 _mask: &[Vec<bool>],
410 horizon: usize,
411 ) -> Result<zsfm_core::QuantileMatrix> {
412 anyhow::ensure!(!context.is_empty(), "context must have at least one variate");
413 let variates: Vec<Vec<f32>> = context
414 .par_iter()
415 .enumerate()
416 .map(|(vi, ctx)| -> Result<Vec<f32>> {
417 anyhow::ensure!(!ctx.is_empty(), "variate {vi} context must not be empty");
418 MoiraiModel::forecast(self, ctx, horizon).with_context(|| format!("forecast variate {vi}"))
419 })
420 .collect::<Result<Vec<_>>>()?;
421 Ok(vec![variates])
422 }
423}