amaye15 commited on
Commit
3e10a23
·
verified ·
1 Parent(s): dbe7899

Upload model files

Browse files
Cargo.toml DELETED
@@ -1,37 +0,0 @@
1
- [package]
2
- name = "flowstate-r1-rs"
3
- version = "0.1.0"
4
- edition = "2021"
5
-
6
- [[bin]]
7
- name = "flowstate-r1-rs"
8
- path = "src/main.rs"
9
-
10
- [dependencies]
11
- pyo3 = { version = "0.29", features = ["extension-module"], optional = true }
12
- reqwest = { version = "0.11", features = ["stream", "json"] }
13
- futures-util = "0.3"
14
- tokio = { version = "1", features = ["full"] }
15
- safetensors = "0.4"
16
- serde = { version = "1", features = ["derive"] }
17
- serde_json = "1"
18
- clap = { version = "4", features = ["derive", "env"] }
19
- anyhow = "1"
20
- byteorder = "1"
21
- indicatif = "0.17"
22
- sha2 = "0.10"
23
- base64 = "0.22"
24
- tokio-util = { version = "0.7", features = ["io"] }
25
- candle-core = { version = "0.8", features = ["accelerate"] }
26
- candle-nn = { version = "0.8", features = ["accelerate"] }
27
-
28
- [profile.release]
29
- opt-level = 3
30
-
31
-
32
- [features]
33
- python = ["dep:pyo3"]
34
-
35
- [lib]
36
- path = "src/lib.rs"
37
- crate-type = ["cdylib", "rlib"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 Andrew Mayes
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
flowstate-r1-rs/Cargo.lock CHANGED
@@ -481,6 +481,7 @@ dependencies = [
481
  "clap",
482
  "futures-util",
483
  "indicatif",
 
484
  "reqwest",
485
  "safetensors",
486
  "serde",
@@ -1552,6 +1553,63 @@ dependencies = [
1552
  "version_check",
1553
  ]
1554
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1555
  [[package]]
1556
  name = "quote"
1557
  version = "1.0.45"
@@ -2013,6 +2071,12 @@ dependencies = [
2013
  "libc",
2014
  ]
2015
 
 
 
 
 
 
 
2016
  [[package]]
2017
  name = "tempfile"
2018
  version = "3.27.0"
 
481
  "clap",
482
  "futures-util",
483
  "indicatif",
484
+ "pyo3",
485
  "reqwest",
486
  "safetensors",
487
  "serde",
 
1553
  "version_check",
1554
  ]
1555
 
1556
+ [[package]]
1557
+ name = "pyo3"
1558
+ version = "0.29.0"
1559
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1560
+ checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
1561
+ dependencies = [
1562
+ "libc",
1563
+ "once_cell",
1564
+ "portable-atomic",
1565
+ "pyo3-build-config",
1566
+ "pyo3-ffi",
1567
+ "pyo3-macros",
1568
+ ]
1569
+
1570
+ [[package]]
1571
+ name = "pyo3-build-config"
1572
+ version = "0.29.0"
1573
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1574
+ checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
1575
+ dependencies = [
1576
+ "target-lexicon",
1577
+ ]
1578
+
1579
+ [[package]]
1580
+ name = "pyo3-ffi"
1581
+ version = "0.29.0"
1582
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1583
+ checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
1584
+ dependencies = [
1585
+ "libc",
1586
+ "pyo3-build-config",
1587
+ ]
1588
+
1589
+ [[package]]
1590
+ name = "pyo3-macros"
1591
+ version = "0.29.0"
1592
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1593
+ checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
1594
+ dependencies = [
1595
+ "proc-macro2",
1596
+ "pyo3-macros-backend",
1597
+ "quote",
1598
+ "syn",
1599
+ ]
1600
+
1601
+ [[package]]
1602
+ name = "pyo3-macros-backend"
1603
+ version = "0.29.0"
1604
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1605
+ checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
1606
+ dependencies = [
1607
+ "heck",
1608
+ "proc-macro2",
1609
+ "quote",
1610
+ "syn",
1611
+ ]
1612
+
1613
  [[package]]
1614
  name = "quote"
1615
  version = "1.0.45"
 
2071
  "libc",
2072
  ]
2073
 
2074
+ [[package]]
2075
+ name = "target-lexicon"
2076
+ version = "0.13.5"
2077
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2078
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
2079
+
2080
  [[package]]
2081
  name = "tempfile"
2082
  version = "3.27.0"
flowstate-r1-rs/Cargo.toml CHANGED
@@ -8,6 +8,7 @@ name = "flowstate-r1-rs"
8
  path = "src/main.rs"
9
 
10
  [dependencies]
 
11
  reqwest = { version = "0.11", features = ["stream", "json"] }
12
  futures-util = "0.3"
13
  tokio = { version = "1", features = ["full"] }
@@ -26,3 +27,11 @@ candle-nn = { version = "0.8", features = ["accelerate"] }
26
 
27
  [profile.release]
28
  opt-level = 3
 
 
 
 
 
 
 
 
 
8
  path = "src/main.rs"
9
 
10
  [dependencies]
11
+ pyo3 = { version = "0.29", features = ["extension-module"], optional = true }
12
  reqwest = { version = "0.11", features = ["stream", "json"] }
13
  futures-util = "0.3"
14
  tokio = { version = "1", features = ["full"] }
 
27
 
28
  [profile.release]
29
  opt-level = 3
30
+
31
+
32
+ [features]
33
+ python = ["dep:pyo3"]
34
+
35
+ [lib]
36
+ path = "src/lib.rs"
37
+ crate-type = ["cdylib", "rlib"]
flowstate-r1-rs/pyproject.toml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["maturin>=1,<2"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "flowstate-r1-rs"
7
+ version = "0.1.0"
8
+ requires-python = ">=3.8"
9
+
10
+ [tool.maturin]
11
+ features = ["python"]
12
+ module-name = "flowstate_r1_rs"
13
+
14
+ [dependency-groups]
15
+ dev = [
16
+ "maturin>=1.14.0",
17
+ ]
flowstate-r1-rs/src/infer/mod.rs CHANGED
@@ -1,8 +1,11 @@
 
 
1
  use std::path::Path;
2
 
3
  use anyhow::Context;
4
- use candle_core::{DType, Device, Tensor};
5
  use candle_core::quantized::gguf_file;
 
6
 
7
  // ---------------------------------------------------------------------------
8
  // Config
@@ -29,30 +32,33 @@ pub struct InferConfig {
29
  struct S5Weights {
30
  log_lambda_real: Vec<f32>, // [state_dim]
31
  lambda_imag: Vec<f32>, // [state_dim]
32
- b_r: Tensor, // [state_dim, embed_dim] (row-major, HF order)
33
  b_i: Tensor, // [state_dim, embed_dim]
34
- c_r: Tensor, // [embed_dim, state_dim] (Python C_tilde shape)
35
  c_i: Tensor, // [embed_dim, state_dim]
36
- d: Vec<f32>, // [embed_dim]
37
  log_delta: Vec<f32>, // [state_dim]
38
  }
39
 
40
  struct BlockWeights {
41
  ssm: S5Weights,
42
- out_weight: Tensor, // [embed_dim, embed_dim]
43
- out_bias: Vec<f32>, // [embed_dim]
44
- norm_weight: Vec<f32>, // [embed_dim]
45
- norm_bias: Vec<f32>, // [embed_dim]
 
 
46
  }
47
 
48
  pub struct FlowStateModel {
49
  device: Device,
50
  config: InferConfig,
51
- embed_w: Tensor, // [embed_dim, n_inputs]
52
- embed_b: Vec<f32>, // [embed_dim]
53
  blocks: Vec<BlockWeights>,
54
- decoder_w: Tensor, // [n_quantiles * decoder_dim, embed_dim]
55
- decoder_b: Vec<f32>, // [n_quantiles * decoder_dim]
 
56
  }
57
 
58
  // ---------------------------------------------------------------------------
@@ -91,11 +97,9 @@ impl FlowStateModel {
91
  .with_context(|| format!("open {}", gguf_path.display()))?;
92
  let content = gguf_file::Content::read(&mut file).context("read GGUF header")?;
93
 
94
- // Embedding
95
  let embed_w = load_matrix(&content, &mut file, "embed.weight", &device)?;
96
- let embed_b = load_f32_vec(&content, &mut file, "embed.bias", &device)?;
97
 
98
- // Encoder blocks
99
  let mut blocks = Vec::with_capacity(config.num_layers);
100
  for n in 0..config.num_layers {
101
  let ssm = S5Weights {
@@ -105,23 +109,32 @@ impl FlowStateModel {
105
  b_i: load_matrix(&content, &mut file, &format!("blk.{n}.ssm.b_i"), &device)?,
106
  c_r: load_matrix(&content, &mut file, &format!("blk.{n}.ssm.c_r"), &device)?,
107
  c_i: load_matrix(&content, &mut file, &format!("blk.{n}.ssm.c_i"), &device)?,
108
- d: load_f32_vec(&content, &mut file, &format!("blk.{n}.ssm.d"), &device)?,
109
  log_delta: load_f32_vec(&content, &mut file, &format!("blk.{n}.ssm.log_delta"), &device)?,
110
  };
111
  blocks.push(BlockWeights {
112
  ssm,
113
- out_weight: load_matrix(&content, &mut file, &format!("blk.{n}.out.weight"), &device)?,
114
- out_bias: load_f32_vec(&content, &mut file, &format!("blk.{n}.out.bias"), &device)?,
115
- norm_weight: load_f32_vec(&content, &mut file, &format!("blk.{n}.norm.weight"), &device)?,
116
- norm_bias: load_f32_vec(&content, &mut file, &format!("blk.{n}.norm.bias"), &device)?,
 
117
  });
118
  }
119
 
120
- // Decoder
121
  let decoder_w = load_matrix(&content, &mut file, "decoder.weight", &device)?;
122
- let decoder_b = load_f32_vec(&content, &mut file, "decoder.bias", &device)?;
123
-
124
- Ok(Self { device, config, embed_w, embed_b, blocks, decoder_w, decoder_b })
 
 
 
 
 
 
 
 
 
125
  }
126
 
127
  // -----------------------------------------------------------------------
@@ -160,29 +173,26 @@ impl FlowStateModel {
160
 
161
  // 4. Embedding: [seq_len, n_inputs] × embed_w^T + embed_b → [seq_len, embed_dim]
162
  let input_t = Tensor::from_vec(input_data, (seq_len, cfg.n_inputs), &self.device)?;
163
- let mut hidden = linear(&input_t, &self.embed_w, &self.embed_b, &self.device)?;
164
- // hidden: [seq_len, embed_dim]
165
 
166
  // 5. Scale factor for discretization: decoder_patch_len / prediction_length
167
  let scale_factor = cfg.decoder_patch_len as f32 / prediction_length as f32;
168
 
169
- // 6. Encoder: 6 S5 layers
170
  let num_layers = self.blocks.len();
171
  for (i, block) in self.blocks.iter().enumerate() {
172
  let is_last = i == num_layers - 1;
173
- hidden = self.apply_s5_layer(hidden, block, scale_factor, is_last, seq_len)?;
174
  }
175
  // After last layer: hidden is [1, embed_dim]
176
 
177
- // 7. Decoder: linear → [1, n_quantiles * decoder_dim]
178
- let coeffs = linear(&hidden, &self.decoder_w, &self.decoder_b, &self.device)?;
179
  let n_q = cfg.quantiles.len();
180
- let coeffs_data: Vec<f32> = coeffs.flatten_all()?.to_vec1()?;
181
- // coeffs_data: [n_quantiles * decoder_dim]
182
 
183
- // Diagnostic: dump all coefficients when FLOWSTATE_DEBUG is set
184
  if std::env::var("FLOWSTATE_DEBUG").is_ok() {
185
- // Print all coefficients as CSV lines to stderr: COEFF,{qi},{d},{val}
186
  for qi in 0..n_q {
187
  for d in 0..cfg.decoder_dim {
188
  eprintln!("COEFF,{qi},{d},{:.8}", coeffs_data[qi * cfg.decoder_dim + d]);
@@ -190,30 +200,28 @@ impl FlowStateModel {
190
  }
191
  }
192
 
193
- // 8. Compute Legendre basis [prediction_length, decoder_dim]
194
- // Python: t[i] = range[0] + i * scale * (range[1]-range[0]) / decoder_patch_len (i=1..n)
195
- let basis = legendre_basis(prediction_length, cfg.decoder_dim, cfg.basis_range,
196
- scale_factor, cfg.decoder_patch_len);
197
- // basis: [prediction_length][decoder_dim], scaled by 1/4
198
-
199
- // 9. Matrix multiply: [n_q, decoder_dim] × [prediction_length, decoder_dim]^T
200
- // = [n_q, prediction_length]
201
- let mut output = vec![vec![0.0f32; prediction_length]; n_q];
202
- for q in 0..n_q {
203
- let coeff_offset = q * cfg.decoder_dim;
204
- for p in 0..prediction_length {
205
- let mut sum = 0.0f32;
206
- for d in 0..cfg.decoder_dim {
207
- sum += coeffs_data[coeff_offset + d] * basis[p][d];
208
- }
209
- output[q][p] = sum;
210
  }
211
- }
 
212
 
213
- // 10. Denormalize: output * final_std + final_mean
 
 
 
 
 
214
  for q in 0..n_q {
215
  for p in 0..prediction_length {
216
- output[q][p] = output[q][p] * final_std + final_mean;
217
  }
218
  }
219
 
@@ -226,11 +234,10 @@ impl FlowStateModel {
226
 
227
  fn apply_s5_layer(
228
  &self,
229
- x: Tensor, // [seq_len, embed_dim] for non-last; [seq_len, embed_dim] → output [1, embed_dim] for last
230
  block: &BlockWeights,
231
  scale_factor: f32,
232
  is_last: bool,
233
- _seq_len: usize,
234
  ) -> anyhow::Result<Tensor> {
235
  let cfg = &self.config;
236
  let state_dim = cfg.state_dim;
@@ -240,81 +247,76 @@ impl FlowStateModel {
240
 
241
  // Save skip connection (trimmed for last layer)
242
  let skip = if is_last {
243
- x.narrow(0, seq_len - 1, 1)? // last row: [1, embed_dim]
244
  } else {
245
  x.clone()
246
  };
247
 
248
- // ---- SSM forward ----
249
-
250
- // Discretize: compute A_bar and B_bar from SSM parameters
251
- let ssm = &block.ssm;
252
- let (a_bar_r, a_bar_i, b_bar_r_data, b_bar_i_data) =
253
- discretize(ssm, scale_factor, state_dim, embed_dim);
 
 
 
 
 
254
 
255
  // B @ x for all timesteps at once: x [seq_len, embed_dim] × B^T [embed_dim, state_dim]
256
- // B_tilde stored as [state_dim, embed_dim], so B @ x = x @ B^T
257
- let b_bar_r_t = Tensor::from_vec(b_bar_r_data, (state_dim, embed_dim), &self.device)?;
258
- let b_bar_i_t = Tensor::from_vec(b_bar_i_data, (state_dim, embed_dim), &self.device)?;
259
-
260
- // x @ B^T → [seq_len, state_dim]
261
- let bu_r = x.matmul(&b_bar_r_t.t()?)?;
262
  let bu_i = x.matmul(&b_bar_i_t.t()?)?;
263
  let bu_r_data: Vec<f32> = bu_r.flatten_all()?.to_vec1()?;
264
  let bu_i_data: Vec<f32> = bu_i.flatten_all()?.to_vec1()?;
265
 
266
- // Sequential SSM scan
267
- let mut h_r = vec![0.0f32; state_dim]; // real part of hidden state
268
- let mut h_i = vec![0.0f32; state_dim]; // imag part
269
- let mut all_h_r = vec![0.0f32; seq_len * state_dim];
270
- let mut all_h_i = vec![0.0f32; seq_len * state_dim];
271
-
272
- for t in 0..seq_len {
273
- for s in 0..state_dim {
274
- let new_r = a_bar_r[s] * h_r[s] - a_bar_i[s] * h_i[s] + bu_r_data[t * state_dim + s];
275
- let new_i = a_bar_r[s] * h_i[s] + a_bar_i[s] * h_r[s] + bu_i_data[t * state_dim + s];
276
- h_r[s] = new_r;
277
- h_i[s] = new_i;
278
  }
279
- all_h_r[t * state_dim..(t + 1) * state_dim].copy_from_slice(&h_r);
280
- all_h_i[t * state_dim..(t + 1) * state_dim].copy_from_slice(&h_i);
281
- }
282
-
283
- // For last layer, keep only the final timestep
284
- let (h_r_t, h_i_t, out_seq_len) = if is_last {
285
- let hr = Tensor::from_vec(all_h_r[(seq_len - 1) * state_dim..].to_vec(), (1, state_dim), &self.device)?;
286
- let hi = Tensor::from_vec(all_h_i[(seq_len - 1) * state_dim..].to_vec(), (1, state_dim), &self.device)?;
287
- (hr, hi, 1usize)
288
  } else {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  let hr = Tensor::from_vec(all_h_r, (seq_len, state_dim), &self.device)?;
290
  let hi = Tensor::from_vec(all_h_i, (seq_len, state_dim), &self.device)?;
291
- (hr, hi, seq_len)
292
  };
293
 
294
- // C @ h: y_real = C_r @ h_r - C_i @ h_i
295
- // C_tilde_r stored as [embed_dim, state_dim]; h_r as [out_seq_len, state_dim]
296
- // y = h @ C^T → [out_seq_len, embed_dim]
297
- let y_from_cr = h_r_t.matmul(&block.ssm.c_r.t()?)?; // [out_seq_len, embed_dim]
298
- let y_from_ci = h_i_t.matmul(&block.ssm.c_i.t()?)?; // [out_seq_len, embed_dim]
299
- let mut y_data: Vec<f32> = (y_from_cr - y_from_ci)?.flatten_all()?.to_vec1()?;
300
 
301
- // Add D skip: y += D * x_at_positions
302
- let x_positions = if is_last {
303
- x.narrow(0, seq_len - 1, 1)?.flatten_all()?.to_vec1::<f32>()?
304
- } else {
305
- x.flatten_all()?.to_vec1::<f32>()?
306
- };
307
- let d = &block.ssm.d;
308
- for t in 0..out_seq_len {
309
- for e in 0..embed_dim {
310
- y_data[t * embed_dim + e] += d[e] * x_positions[t * embed_dim + e];
311
- }
312
- }
313
 
314
  // ---- MLP: selu(y) * sigmoid(out_linear(selu(y))) ----
315
- let y_t = Tensor::from_vec(y_data, (out_seq_len, embed_dim), &self.device)?;
316
  let y_selu = selu_tensor(&y_t)?;
317
- let gate_pre = linear(&y_selu, &block.out_weight, &block.out_bias, &self.device)?;
318
  let gate = sigmoid_tensor(&gate_pre)?;
319
  let y_gated = y_selu.mul(&gate)?;
320
 
@@ -322,9 +324,7 @@ impl FlowStateModel {
322
  let y_normed = layer_norm(&y_gated, &block.norm_weight, &block.norm_bias, self.config.eps)?;
323
 
324
  // ---- Residual ----
325
- let out = (y_normed + skip)?;
326
-
327
- Ok(out)
328
  }
329
  }
330
 
@@ -347,7 +347,6 @@ fn causal_revin_norm(x: &[f32], eps: f32) -> (Vec<f32>, f32, f32) {
347
  cum_sum += x[t];
348
  let mean_t = cum_sum / count;
349
 
350
- // Python: cumsum((x[s] - mean_s)^2) / count — each step uses its own running mean
351
  cum_sq_diff += (x[t] - mean_t) * (x[t] - mean_t);
352
  let var_t = (cum_sq_diff / count).max(0.0);
353
  let std_t = (var_t + eps).sqrt();
@@ -367,23 +366,17 @@ fn causal_revin_norm(x: &[f32], eps: f32) -> (Vec<f32>, f32, f32) {
367
  // SSM discretization
368
  // ---------------------------------------------------------------------------
369
 
370
- /// Returns (A_bar_real, A_bar_imag, B_bar_real_flat, B_bar_imag_flat).
371
- /// B_bar shape: [state_dim, embed_dim] flattened row-major.
372
  fn discretize(
373
  ssm: &S5Weights,
374
  scale_factor: f32,
375
  state_dim: usize,
376
  embed_dim: usize,
377
- ) -> (Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>) {
378
- // lambda = -exp(log_lambda_real) + j * lambda_imag
379
- // delta = exp(scale_factor * log_delta)
380
- // A_bar = exp(lambda * delta) = exp((lambda_r + j*lambda_i) * delta)
381
- // = exp(lambda_r * delta) * (cos(lambda_i * delta) + j*sin(lambda_i * delta))
382
-
383
  let mut a_r = vec![0.0f32; state_dim];
384
  let mut a_i = vec![0.0f32; state_dim];
385
-
386
- // (A_bar - 1) / lambda per element (complex division)
387
  let mut coeff_r = vec![0.0f32; state_dim];
388
  let mut coeff_i = vec![0.0f32; state_dim];
389
 
@@ -392,16 +385,12 @@ fn discretize(
392
  let lam_i = ssm.lambda_imag[s];
393
  let delta = (scale_factor * ssm.log_delta[s]).exp();
394
 
395
- // A_bar = exp(lambda * delta)
396
  let exp_r = lam_r * delta;
397
  let exp_i = lam_i * delta;
398
  let mag = exp_r.exp();
399
  a_r[s] = mag * exp_i.cos();
400
  a_i[s] = mag * exp_i.sin();
401
 
402
- // (A_bar - 1) / lambda
403
- // numerator: (a_r - 1) + j*a_i
404
- // denominator: lam_r + j*lam_i
405
  let num_r = a_r[s] - 1.0;
406
  let num_i = a_i[s];
407
  let denom_sq = lam_r * lam_r + lam_i * lam_i;
@@ -409,31 +398,14 @@ fn discretize(
409
  coeff_r[s] = (num_r * lam_r + num_i * lam_i) / denom_sq;
410
  coeff_i[s] = (num_i * lam_r - num_r * lam_i) / denom_sq;
411
  } else {
412
- coeff_r[s] = delta; // limit as lambda → 0
413
  coeff_i[s] = 0.0;
414
  }
415
  }
416
 
417
- // B_bar = coeff * B_tilde (element-wise complex multiply, then multiply B matrix)
418
- // B_tilde stored as [state_dim, embed_dim] in GGUF (transposed from Python HF which is also [state_dim, emb])
419
- // After load_matrix, candle reverses dims: stored [embed_dim, state_dim] in GGUF → loaded as [state_dim, embed_dim]
420
- // Actually GGUF reversal: Python [state_dim, embed_dim] → GGUF stores reversed → candle sees [embed_dim, state_dim]
421
- // But we treat it as [state_dim, embed_dim] for the einsum B @ u = u @ B^T
422
- // Let's get B_tilde data and compute B_bar manually
423
- // The GGUF shape is reversed from Python. Python B_tilde: [512, 512].
424
- // So GGUF/candle sees it as [512, 512] (square, so same either way).
425
-
426
- // Get B_tilde data as flat arrays [state_dim * embed_dim]
427
- // b_r and b_i are loaded as candle tensors; we need them as flat vecs for the per-state multiply
428
- // They were stored with reversed shape but 512×512 is symmetric in shape
429
- // We extract row by row: B_tilde[s, :] = row s of the [state_dim, embed_dim] matrix
430
-
431
- // Use flat data from the tensors
432
  let b_r_data = get_tensor_data_row_major(&ssm.b_r, state_dim, embed_dim);
433
  let b_i_data = get_tensor_data_row_major(&ssm.b_i, state_dim, embed_dim);
434
 
435
- // B_bar[s, e] = coeff_r[s]*b_r[s,e] - coeff_i[s]*b_i[s,e] (real)
436
- // + coeff_r[s]*b_i[s,e] + coeff_i[s]*b_r[s,e] (imag)
437
  let mut b_bar_r = vec![0.0f32; state_dim * embed_dim];
438
  let mut b_bar_i = vec![0.0f32; state_dim * embed_dim];
439
 
@@ -446,11 +418,13 @@ fn discretize(
446
  }
447
  }
448
 
449
- (a_r, a_i, b_bar_r, b_bar_i)
 
 
 
450
  }
451
 
452
  /// Extract tensor data in row-major order as Vec<f32>.
453
- /// Handles candle's internal memory layout for F32 tensors.
454
  fn get_tensor_data_row_major(t: &Tensor, rows: usize, cols: usize) -> Vec<f32> {
455
  t.to_dtype(DType::F32)
456
  .and_then(|t| t.reshape((rows, cols)))
@@ -471,14 +445,13 @@ pub fn dump_legendre_basis(n_points: usize, degree: usize, range: [f32; 2],
471
 
472
  /// Compute Legendre polynomial basis matrix.
473
  /// Returns [n_points][degree+1] scaled by 1/4 (as in get_kernel).
474
- /// Python formula: t[i] = range[0] + i * scale * (range[1]-range[0]) / pred_dist (i=1..n_points)
475
  fn legendre_basis(n_points: usize, degree: usize, range: [f32; 2],
476
  scale: f32, pred_dist: usize) -> Vec<Vec<f32>> {
477
  let dt = scale * (range[1] - range[0]) / pred_dist as f32;
478
  let t: Vec<f32> = (1..=n_points).map(|i| range[0] + i as f32 * dt).collect();
479
 
480
- // Evaluate Legendre polynomials at each t: [n_points, degree+1]
481
- // P_0 = 1, P_1 = x, P_k = ((2k-1)*x*P_{k-1} - (k-1)*P_{k-2}) / k
482
  let mut basis = vec![vec![0.0f32; degree + 1]; n_points];
483
  for (p, &x) in t.iter().enumerate() {
484
  basis[p][0] = 1.0;
@@ -490,10 +463,10 @@ fn legendre_basis(n_points: usize, degree: usize, range: [f32; 2],
490
  basis[p][k + 1] =
491
  ((2.0 * kf + 1.0) * x * basis[p][k] - kf * basis[p][k - 1]) / (kf + 1.0);
492
  }
493
- // Scale by 1/4 (from get_kernel: return f.float() / 4.0)
494
- for d in 0..=degree {
495
  basis[p][d] /= 4.0;
496
  }
 
497
  }
498
 
499
  basis
@@ -503,55 +476,33 @@ fn legendre_basis(n_points: usize, degree: usize, range: [f32; 2],
503
  // Neural network primitives
504
  // ---------------------------------------------------------------------------
505
 
506
- /// y = x @ w^T + b (w: [out, in])
507
- fn linear(x: &Tensor, w: &Tensor, b: &[f32], device: &Device) -> anyhow::Result<Tensor> {
508
- let out_size = b.len();
509
- let y = x.matmul(&w.t()?)?;
510
- let b_t = Tensor::from_vec(b.to_vec(), (1, out_size), device)?.broadcast_as(y.shape())?;
511
- Ok((y + b_t)?)
512
  }
513
 
514
- /// SELU activation: selu(x) = scale * (x if x>0 else alpha*(exp(x)-1))
515
  fn selu_tensor(x: &Tensor) -> anyhow::Result<Tensor> {
516
  const SCALE: f64 = 1.0507009873554804934193349852946;
517
  const ALPHA: f64 = 1.6732632423543772848170429916717;
518
- let data: Vec<f32> = x.flatten_all()?.to_vec1()?;
519
- let selu_data: Vec<f32> = data.iter().map(|&v| {
520
- if v > 0.0 {
521
- (SCALE * v as f64) as f32
522
- } else {
523
- (SCALE * ALPHA * (v.exp() as f64 - 1.0)) as f32
524
- }
525
- }).collect();
526
- Ok(Tensor::from_vec(selu_data, x.shape(), x.device())?)
527
  }
528
 
529
- /// Sigmoid: 1 / (1 + exp(-x))
530
  fn sigmoid_tensor(x: &Tensor) -> anyhow::Result<Tensor> {
531
- let data: Vec<f32> = x.flatten_all()?.to_vec1()?;
532
- let sig_data: Vec<f32> = data.iter().map(|&v| 1.0 / (1.0 + (-v).exp())).collect();
533
- Ok(Tensor::from_vec(sig_data, x.shape(), x.device())?)
534
  }
535
 
536
- /// LayerNorm: (x - mean) / std * weight + bias (over last dimension)
537
- fn layer_norm(x: &Tensor, weight: &[f32], bias: &[f32], eps: f32) -> anyhow::Result<Tensor> {
538
- let shape = x.shape().clone();
539
- let last_dim = *shape.dims().last().context("empty tensor")?;
540
- let n = shape.elem_count();
541
- let n_rows = n / last_dim;
542
-
543
- let data: Vec<f32> = x.flatten_all()?.to_vec1()?;
544
- let mut out = vec![0.0f32; n];
545
-
546
- for r in 0..n_rows {
547
- let row = &data[r * last_dim..(r + 1) * last_dim];
548
- let mean: f32 = row.iter().sum::<f32>() / last_dim as f32;
549
- let var: f32 = row.iter().map(|&v| (v - mean).powi(2)).sum::<f32>() / last_dim as f32;
550
- let std_inv = 1.0 / (var + eps).sqrt();
551
- for i in 0..last_dim {
552
- out[r * last_dim + i] = (row[i] - mean) * std_inv * weight[i] + bias[i];
553
- }
554
- }
555
-
556
- Ok(Tensor::from_vec(out, shape, x.device())?)
557
  }
 
1
+ use std::collections::HashMap;
2
+ use std::sync::Mutex;
3
  use std::path::Path;
4
 
5
  use anyhow::Context;
6
+ use candle_core::{DType, Device, Tensor, D};
7
  use candle_core::quantized::gguf_file;
8
+ use candle_nn::ops;
9
 
10
  // ---------------------------------------------------------------------------
11
  // Config
 
32
  struct S5Weights {
33
  log_lambda_real: Vec<f32>, // [state_dim]
34
  lambda_imag: Vec<f32>, // [state_dim]
35
+ b_r: Tensor, // [state_dim, embed_dim]
36
  b_i: Tensor, // [state_dim, embed_dim]
37
+ c_r: Tensor, // [embed_dim, state_dim]
38
  c_i: Tensor, // [embed_dim, state_dim]
39
+ d: Tensor, // [embed_dim]
40
  log_delta: Vec<f32>, // [state_dim]
41
  }
42
 
43
  struct BlockWeights {
44
  ssm: S5Weights,
45
+ out_weight: Tensor, // [embed_dim, embed_dim]
46
+ out_bias: Tensor, // [embed_dim]
47
+ norm_weight: Tensor, // [embed_dim]
48
+ norm_bias: Tensor, // [embed_dim]
49
+ // cached per scale_factor: (A_bar_real, A_bar_imag, B_bar_real_t, B_bar_imag_t)
50
+ disc_cache: Mutex<HashMap<u32, (Vec<f32>, Vec<f32>, Tensor, Tensor)>>,
51
  }
52
 
53
  pub struct FlowStateModel {
54
  device: Device,
55
  config: InferConfig,
56
+ embed_w: Tensor, // [embed_dim, n_inputs]
57
+ embed_b: Tensor, // [embed_dim]
58
  blocks: Vec<BlockWeights>,
59
+ decoder_w: Tensor, // [n_quantiles * decoder_dim, embed_dim]
60
+ decoder_b: Tensor, // [n_quantiles * decoder_dim]
61
+ legendre_cache: Mutex<HashMap<usize, Tensor>>,
62
  }
63
 
64
  // ---------------------------------------------------------------------------
 
97
  .with_context(|| format!("open {}", gguf_path.display()))?;
98
  let content = gguf_file::Content::read(&mut file).context("read GGUF header")?;
99
 
 
100
  let embed_w = load_matrix(&content, &mut file, "embed.weight", &device)?;
101
+ let embed_b = load_matrix(&content, &mut file, "embed.bias", &device)?;
102
 
 
103
  let mut blocks = Vec::with_capacity(config.num_layers);
104
  for n in 0..config.num_layers {
105
  let ssm = S5Weights {
 
109
  b_i: load_matrix(&content, &mut file, &format!("blk.{n}.ssm.b_i"), &device)?,
110
  c_r: load_matrix(&content, &mut file, &format!("blk.{n}.ssm.c_r"), &device)?,
111
  c_i: load_matrix(&content, &mut file, &format!("blk.{n}.ssm.c_i"), &device)?,
112
+ d: load_matrix(&content, &mut file, &format!("blk.{n}.ssm.d"), &device)?,
113
  log_delta: load_f32_vec(&content, &mut file, &format!("blk.{n}.ssm.log_delta"), &device)?,
114
  };
115
  blocks.push(BlockWeights {
116
  ssm,
117
+ out_weight: load_matrix(&content, &mut file, &format!("blk.{n}.out.weight"), &device)?,
118
+ out_bias: load_matrix(&content, &mut file, &format!("blk.{n}.out.bias"), &device)?,
119
+ norm_weight: load_matrix(&content, &mut file, &format!("blk.{n}.norm.weight"), &device)?,
120
+ norm_bias: load_matrix(&content, &mut file, &format!("blk.{n}.norm.bias"), &device)?,
121
+ disc_cache: Mutex::new(HashMap::new()),
122
  });
123
  }
124
 
 
125
  let decoder_w = load_matrix(&content, &mut file, "decoder.weight", &device)?;
126
+ let decoder_b = load_matrix(&content, &mut file, "decoder.bias", &device)?;
127
+
128
+ Ok(Self {
129
+ device,
130
+ config,
131
+ embed_w,
132
+ embed_b,
133
+ blocks,
134
+ decoder_w,
135
+ decoder_b,
136
+ legendre_cache: Mutex::new(HashMap::new()),
137
+ })
138
  }
139
 
140
  // -----------------------------------------------------------------------
 
173
 
174
  // 4. Embedding: [seq_len, n_inputs] × embed_w^T + embed_b → [seq_len, embed_dim]
175
  let input_t = Tensor::from_vec(input_data, (seq_len, cfg.n_inputs), &self.device)?;
176
+ let mut hidden = linear(&input_t, &self.embed_w, &self.embed_b)?;
 
177
 
178
  // 5. Scale factor for discretization: decoder_patch_len / prediction_length
179
  let scale_factor = cfg.decoder_patch_len as f32 / prediction_length as f32;
180
 
181
+ // 6. Encoder: S5 layers
182
  let num_layers = self.blocks.len();
183
  for (i, block) in self.blocks.iter().enumerate() {
184
  let is_last = i == num_layers - 1;
185
+ hidden = self.apply_s5_layer(hidden, block, scale_factor, is_last)?;
186
  }
187
  // After last layer: hidden is [1, embed_dim]
188
 
189
+ // 7. Decoder: linear → [n_q, decoder_dim]
 
190
  let n_q = cfg.quantiles.len();
191
+ let coeffs = linear(&hidden, &self.decoder_w, &self.decoder_b)?
192
+ .reshape((n_q, cfg.decoder_dim))?;
193
 
 
194
  if std::env::var("FLOWSTATE_DEBUG").is_ok() {
195
+ let coeffs_data: Vec<f32> = coeffs.flatten_all()?.to_vec1()?;
196
  for qi in 0..n_q {
197
  for d in 0..cfg.decoder_dim {
198
  eprintln!("COEFF,{qi},{d},{:.8}", coeffs_data[qi * cfg.decoder_dim + d]);
 
200
  }
201
  }
202
 
203
+ // 8. Legendre basis [prediction_length, decoder_dim] — cached per prediction_length
204
+ let basis = {
205
+ let mut cache = self.legendre_cache.lock().unwrap();
206
+ if !cache.contains_key(&prediction_length) {
207
+ let raw = legendre_basis(prediction_length, cfg.decoder_dim, cfg.basis_range,
208
+ scale_factor, cfg.decoder_patch_len);
209
+ let flat: Vec<f32> = raw.into_iter().flatten().collect();
210
+ cache.insert(prediction_length,
211
+ Tensor::from_vec(flat, (prediction_length, cfg.decoder_dim), &self.device)?);
 
 
 
 
 
 
 
 
212
  }
213
+ cache[&prediction_length].clone()
214
+ };
215
 
216
+ // 9. [n_q, decoder_dim] @ [decoder_dim, prediction_length] → [n_q, prediction_length]
217
+ let out_t = coeffs.matmul(&basis.t()?)?;
218
+
219
+ // 10. Denormalize and convert to Vec<Vec<f32>>
220
+ let out_raw: Vec<f32> = out_t.flatten_all()?.to_vec1()?;
221
+ let mut output = vec![vec![0.0f32; prediction_length]; n_q];
222
  for q in 0..n_q {
223
  for p in 0..prediction_length {
224
+ output[q][p] = out_raw[q * prediction_length + p] * final_std + final_mean;
225
  }
226
  }
227
 
 
234
 
235
  fn apply_s5_layer(
236
  &self,
237
+ x: Tensor, // [seq_len, embed_dim]
238
  block: &BlockWeights,
239
  scale_factor: f32,
240
  is_last: bool,
 
241
  ) -> anyhow::Result<Tensor> {
242
  let cfg = &self.config;
243
  let state_dim = cfg.state_dim;
 
247
 
248
  // Save skip connection (trimmed for last layer)
249
  let skip = if is_last {
250
+ x.narrow(0, seq_len - 1, 1)? // [1, embed_dim]
251
  } else {
252
  x.clone()
253
  };
254
 
255
+ // ---- Get or compute discretized SSM matrices (cached per scale_factor) ----
256
+ let (a_bar_r, a_bar_i, b_bar_r_t, b_bar_i_t) = {
257
+ let key = scale_factor.to_bits();
258
+ let mut cache = block.disc_cache.lock().unwrap();
259
+ if !cache.contains_key(&key) {
260
+ let result = discretize(&block.ssm, scale_factor, state_dim, embed_dim, &self.device)?;
261
+ cache.insert(key, result);
262
+ }
263
+ let (ar, ai, brt, bit) = &cache[&key];
264
+ (ar.clone(), ai.clone(), brt.clone(), bit.clone())
265
+ };
266
 
267
  // B @ x for all timesteps at once: x [seq_len, embed_dim] × B^T [embed_dim, state_dim]
268
+ let bu_r = x.matmul(&b_bar_r_t.t()?)?; // [seq_len, state_dim]
 
 
 
 
 
269
  let bu_i = x.matmul(&b_bar_i_t.t()?)?;
270
  let bu_r_data: Vec<f32> = bu_r.flatten_all()?.to_vec1()?;
271
  let bu_i_data: Vec<f32> = bu_i.flatten_all()?.to_vec1()?;
272
 
273
+ // Sequential SSM scan (inherently sequential — recurrence prevents parallelism).
274
+ // For the last block only the final hidden state is needed, so skip the history buffer.
275
+ let (h_r_t, h_i_t) = if is_last {
276
+ let mut h_r = vec![0.0f32; state_dim];
277
+ let mut h_i = vec![0.0f32; state_dim];
278
+ for t in 0..seq_len {
279
+ for s in 0..state_dim {
280
+ let new_r = a_bar_r[s] * h_r[s] - a_bar_i[s] * h_i[s] + bu_r_data[t * state_dim + s];
281
+ let new_i = a_bar_r[s] * h_i[s] + a_bar_i[s] * h_r[s] + bu_i_data[t * state_dim + s];
282
+ h_r[s] = new_r;
283
+ h_i[s] = new_i;
284
+ }
285
  }
286
+ let hr = Tensor::from_vec(h_r, (1, state_dim), &self.device)?;
287
+ let hi = Tensor::from_vec(h_i, (1, state_dim), &self.device)?;
288
+ (hr, hi)
 
 
 
 
 
 
289
  } else {
290
+ let mut h_r = vec![0.0f32; state_dim];
291
+ let mut h_i = vec![0.0f32; state_dim];
292
+ let mut all_h_r = vec![0.0f32; seq_len * state_dim];
293
+ let mut all_h_i = vec![0.0f32; seq_len * state_dim];
294
+ for t in 0..seq_len {
295
+ for s in 0..state_dim {
296
+ let new_r = a_bar_r[s] * h_r[s] - a_bar_i[s] * h_i[s] + bu_r_data[t * state_dim + s];
297
+ let new_i = a_bar_r[s] * h_i[s] + a_bar_i[s] * h_r[s] + bu_i_data[t * state_dim + s];
298
+ h_r[s] = new_r;
299
+ h_i[s] = new_i;
300
+ }
301
+ all_h_r[t * state_dim..(t + 1) * state_dim].copy_from_slice(&h_r);
302
+ all_h_i[t * state_dim..(t + 1) * state_dim].copy_from_slice(&h_i);
303
+ }
304
  let hr = Tensor::from_vec(all_h_r, (seq_len, state_dim), &self.device)?;
305
  let hi = Tensor::from_vec(all_h_i, (seq_len, state_dim), &self.device)?;
306
+ (hr, hi)
307
  };
308
 
309
+ // C @ h: y_real = C_r @ h_r - C_i @ h_i → [out_seq_len, embed_dim]
310
+ let y_from_cr = h_r_t.matmul(&block.ssm.c_r.t()?)?;
311
+ let y_from_ci = h_i_t.matmul(&block.ssm.c_i.t()?)?;
312
+ let y_raw = (y_from_cr - y_from_ci)?;
 
 
313
 
314
+ // D skip: y += D * x_at_positions (skip is the correct slice in both cases)
315
+ let y_t = (y_raw + skip.broadcast_mul(&block.ssm.d)?)?;
 
 
 
 
 
 
 
 
 
 
316
 
317
  // ---- MLP: selu(y) * sigmoid(out_linear(selu(y))) ----
 
318
  let y_selu = selu_tensor(&y_t)?;
319
+ let gate_pre = linear(&y_selu, &block.out_weight, &block.out_bias)?;
320
  let gate = sigmoid_tensor(&gate_pre)?;
321
  let y_gated = y_selu.mul(&gate)?;
322
 
 
324
  let y_normed = layer_norm(&y_gated, &block.norm_weight, &block.norm_bias, self.config.eps)?;
325
 
326
  // ---- Residual ----
327
+ Ok((y_normed + skip)?)
 
 
328
  }
329
  }
330
 
 
347
  cum_sum += x[t];
348
  let mean_t = cum_sum / count;
349
 
 
350
  cum_sq_diff += (x[t] - mean_t) * (x[t] - mean_t);
351
  let var_t = (cum_sq_diff / count).max(0.0);
352
  let std_t = (var_t + eps).sqrt();
 
366
  // SSM discretization
367
  // ---------------------------------------------------------------------------
368
 
369
+ /// Returns (A_bar_real, A_bar_imag, B_bar_real_tensor, B_bar_imag_tensor).
370
+ /// B_bar tensors have shape [state_dim, embed_dim].
371
  fn discretize(
372
  ssm: &S5Weights,
373
  scale_factor: f32,
374
  state_dim: usize,
375
  embed_dim: usize,
376
+ device: &Device,
377
+ ) -> anyhow::Result<(Vec<f32>, Vec<f32>, Tensor, Tensor)> {
 
 
 
 
378
  let mut a_r = vec![0.0f32; state_dim];
379
  let mut a_i = vec![0.0f32; state_dim];
 
 
380
  let mut coeff_r = vec![0.0f32; state_dim];
381
  let mut coeff_i = vec![0.0f32; state_dim];
382
 
 
385
  let lam_i = ssm.lambda_imag[s];
386
  let delta = (scale_factor * ssm.log_delta[s]).exp();
387
 
 
388
  let exp_r = lam_r * delta;
389
  let exp_i = lam_i * delta;
390
  let mag = exp_r.exp();
391
  a_r[s] = mag * exp_i.cos();
392
  a_i[s] = mag * exp_i.sin();
393
 
 
 
 
394
  let num_r = a_r[s] - 1.0;
395
  let num_i = a_i[s];
396
  let denom_sq = lam_r * lam_r + lam_i * lam_i;
 
398
  coeff_r[s] = (num_r * lam_r + num_i * lam_i) / denom_sq;
399
  coeff_i[s] = (num_i * lam_r - num_r * lam_i) / denom_sq;
400
  } else {
401
+ coeff_r[s] = delta;
402
  coeff_i[s] = 0.0;
403
  }
404
  }
405
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  let b_r_data = get_tensor_data_row_major(&ssm.b_r, state_dim, embed_dim);
407
  let b_i_data = get_tensor_data_row_major(&ssm.b_i, state_dim, embed_dim);
408
 
 
 
409
  let mut b_bar_r = vec![0.0f32; state_dim * embed_dim];
410
  let mut b_bar_i = vec![0.0f32; state_dim * embed_dim];
411
 
 
418
  }
419
  }
420
 
421
+ let b_bar_r_t = Tensor::from_vec(b_bar_r, (state_dim, embed_dim), device)?;
422
+ let b_bar_i_t = Tensor::from_vec(b_bar_i, (state_dim, embed_dim), device)?;
423
+
424
+ Ok((a_r, a_i, b_bar_r_t, b_bar_i_t))
425
  }
426
 
427
  /// Extract tensor data in row-major order as Vec<f32>.
 
428
  fn get_tensor_data_row_major(t: &Tensor, rows: usize, cols: usize) -> Vec<f32> {
429
  t.to_dtype(DType::F32)
430
  .and_then(|t| t.reshape((rows, cols)))
 
445
 
446
  /// Compute Legendre polynomial basis matrix.
447
  /// Returns [n_points][degree+1] scaled by 1/4 (as in get_kernel).
 
448
  fn legendre_basis(n_points: usize, degree: usize, range: [f32; 2],
449
  scale: f32, pred_dist: usize) -> Vec<Vec<f32>> {
450
  let dt = scale * (range[1] - range[0]) / pred_dist as f32;
451
  let t: Vec<f32> = (1..=n_points).map(|i| range[0] + i as f32 * dt).collect();
452
 
453
+ // Compute degree Legendre polynomials (P0..P_{degree-1}) per point.
454
+ // degree+1 scratch columns needed during recurrence, then truncated to degree.
455
  let mut basis = vec![vec![0.0f32; degree + 1]; n_points];
456
  for (p, &x) in t.iter().enumerate() {
457
  basis[p][0] = 1.0;
 
463
  basis[p][k + 1] =
464
  ((2.0 * kf + 1.0) * x * basis[p][k] - kf * basis[p][k - 1]) / (kf + 1.0);
465
  }
466
+ for d in 0..degree {
 
467
  basis[p][d] /= 4.0;
468
  }
469
+ basis[p].truncate(degree);
470
  }
471
 
472
  basis
 
476
  // Neural network primitives
477
  // ---------------------------------------------------------------------------
478
 
479
+ /// y = x @ w^T + b (w: [out, in], b: [out])
480
+ fn linear(x: &Tensor, w: &Tensor, b: &Tensor) -> anyhow::Result<Tensor> {
481
+ Ok(x.matmul(&w.t()?)?.broadcast_add(b)?)
 
 
 
482
  }
483
 
484
+ /// SELU activation using candle ops (no Vec roundtrip).
485
  fn selu_tensor(x: &Tensor) -> anyhow::Result<Tensor> {
486
  const SCALE: f64 = 1.0507009873554804934193349852946;
487
  const ALPHA: f64 = 1.6732632423543772848170429916717;
488
+ const ALPHA_SCALE: f64 = SCALE * ALPHA;
489
+ let pos = x.relu()?;
490
+ let neg = (x - &pos)?; // min(x, 0)
491
+ let selu_pos = (pos * SCALE)?;
492
+ let selu_neg = ((neg.exp()? - 1.0)? * ALPHA_SCALE)?;
493
+ Ok((selu_pos + selu_neg)?)
 
 
 
494
  }
495
 
 
496
  fn sigmoid_tensor(x: &Tensor) -> anyhow::Result<Tensor> {
497
+ Ok(ops::sigmoid(x)?)
 
 
498
  }
499
 
500
+ /// LayerNorm using candle ops (no Vec roundtrip).
501
+ fn layer_norm(x: &Tensor, weight: &Tensor, bias: &Tensor, eps: f32) -> anyhow::Result<Tensor> {
502
+ let mean = x.mean_keepdim(D::Minus1)?;
503
+ let x_c = x.broadcast_sub(&mean)?;
504
+ let var = x_c.sqr()?.mean_keepdim(D::Minus1)?;
505
+ let std = (var + eps as f64)?.sqrt()?;
506
+ let normed = x_c.broadcast_div(&std)?;
507
+ Ok(normed.broadcast_mul(weight)?.broadcast_add(bias)?)
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  }
{src → flowstate-r1-rs/src}/lib.rs RENAMED
File without changes
flowstate-r1-rs/src/main.rs CHANGED
@@ -184,7 +184,7 @@ async fn main() -> anyhow::Result<()> {
184
  let mut buf = String::new();
185
  std::io::stdin().read_to_string(&mut buf).context("read stdin")?;
186
  let req: serde_json::Value = serde_json::from_str(&buf).context("parse JSON input")?;
187
- let ctx: Vec<f32> = serde_json::from_value(req["context"].clone()).context("context must be a JSON array of numbers")?;
188
  let horizon: usize = req["horizon"].as_u64().context("horizon must be a positive integer")? as usize;
189
 
190
  let config_str = std::fs::read_to_string(&config)
@@ -205,47 +205,68 @@ async fn main() -> anyhow::Result<()> {
205
  eps: 1e-5,
206
  };
207
 
208
- anyhow::ensure!(!ctx.is_empty(), "No context values provided");
209
-
210
- eprintln!("Loading model from {} …", gguf.display());
211
- let model = FlowStateModel::load(&gguf, infer_config.clone())
212
- .context("load model")?;
213
-
214
- eprintln!("Running forecast ({} context steps → {horizon} future steps) …", ctx.len());
215
- let quantile_mat = model.forecast(&ctx, horizon)
216
- .context("forecast")?;
217
- // quantile_mat: [n_quantiles][horizon]
218
-
219
- let quantile_levels = &infer_config.quantiles;
220
-
221
- // Find median index (q0.5), fall back to middle
222
  let median_idx = quantile_levels
223
  .iter()
224
  .position(|&q| (q - 0.5).abs() < 1e-6)
225
  .unwrap_or(quantile_levels.len() / 2);
226
 
227
- let point = quantile_mat.get(median_idx).cloned().unwrap_or_default();
 
 
228
 
229
- let mut quantiles = BTreeMap::new();
230
- for (i, &level) in quantile_levels.iter().enumerate() {
231
- if let Some(q) = quantile_mat.get(i) {
232
- quantiles.insert(format!("{level:.2}"), q.clone());
 
 
 
 
 
 
 
 
233
  }
 
234
  }
235
-
236
- println!("{}", forecast_json("flowstate-r1", ctx.len(), horizon, point, quantiles)?);
237
  }
238
  }
239
 
240
  Ok(())
241
  }
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  fn forecast_json(
244
  model_name: &str,
245
  context_length: usize,
246
  forecast_length: usize,
247
- point: Vec<f32>,
248
- quantiles: BTreeMap<String, Vec<f32>>,
249
  ) -> anyhow::Result<String> {
250
  #[derive(Serialize)]
251
  struct ForecastResponse {
@@ -283,11 +304,11 @@ fn forecast_json(
283
  object: "forecast",
284
  created,
285
  model: model_name.to_string(),
286
- choices: vec![Choice {
287
- index: 0,
288
  forecast: ForecastOutput { point, quantiles },
289
  finish_reason: "stop",
290
- }],
291
  usage: Usage { context_length, forecast_length },
292
  };
293
 
 
184
  let mut buf = String::new();
185
  std::io::stdin().read_to_string(&mut buf).context("read stdin")?;
186
  let req: serde_json::Value = serde_json::from_str(&buf).context("parse JSON input")?;
187
+ let contexts = parse_contexts(req["context"].clone())?;
188
  let horizon: usize = req["horizon"].as_u64().context("horizon must be a positive integer")? as usize;
189
 
190
  let config_str = std::fs::read_to_string(&config)
 
205
  eps: 1e-5,
206
  };
207
 
208
+ let quantile_levels = infer_config.quantiles.clone();
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  let median_idx = quantile_levels
210
  .iter()
211
  .position(|&q| (q - 0.5).abs() < 1e-6)
212
  .unwrap_or(quantile_levels.len() / 2);
213
 
214
+ eprintln!("Loading model from {} …", gguf.display());
215
+ let model = FlowStateModel::load(&gguf, infer_config)
216
+ .context("load model")?;
217
 
218
+ let mut fc_choices = Vec::new();
219
+ for ctx in &contexts {
220
+ anyhow::ensure!(!ctx.is_empty(), "context series must not be empty");
221
+ eprintln!("Running forecast ({} context steps → {horizon} future steps) …", ctx.len());
222
+ let quantile_mat = model.forecast(ctx, horizon).context("forecast")?;
223
+
224
+ let point = quantile_mat.get(median_idx).cloned().unwrap_or_default();
225
+ let mut quantiles = BTreeMap::new();
226
+ for (i, &level) in quantile_levels.iter().enumerate() {
227
+ if let Some(q) = quantile_mat.get(i) {
228
+ quantiles.insert(format!("{level:.2}"), q.clone());
229
+ }
230
  }
231
+ fc_choices.push((point, quantiles));
232
  }
233
+ let total_ctx: usize = contexts.iter().map(|c| c.len()).sum();
234
+ println!("{}", forecast_json("flowstate-r1", total_ctx, horizon, fc_choices)?);
235
  }
236
  }
237
 
238
  Ok(())
239
  }
240
 
241
+ fn parse_contexts(val: serde_json::Value) -> anyhow::Result<Vec<Vec<f32>>> {
242
+ match val {
243
+ serde_json::Value::Array(arr) if arr.is_empty() => {
244
+ anyhow::bail!("context must be a non-empty array")
245
+ }
246
+ serde_json::Value::Array(arr) => {
247
+ if arr.first().map(|v| v.is_array()).unwrap_or(false) {
248
+ arr.into_iter()
249
+ .enumerate()
250
+ .map(|(i, v)| {
251
+ serde_json::from_value::<Vec<f32>>(v)
252
+ .with_context(|| format!("context[{i}] must be an array of numbers"))
253
+ })
254
+ .collect()
255
+ } else {
256
+ let ctx = serde_json::from_value::<Vec<f32>>(serde_json::Value::Array(arr))
257
+ .context("context must be a JSON array of numbers")?;
258
+ Ok(vec![ctx])
259
+ }
260
+ }
261
+ _ => anyhow::bail!("context must be a JSON array"),
262
+ }
263
+ }
264
+
265
  fn forecast_json(
266
  model_name: &str,
267
  context_length: usize,
268
  forecast_length: usize,
269
+ fc_choices: Vec<(Vec<f32>, BTreeMap<String, Vec<f32>>)>,
 
270
  ) -> anyhow::Result<String> {
271
  #[derive(Serialize)]
272
  struct ForecastResponse {
 
304
  object: "forecast",
305
  created,
306
  model: model_name.to_string(),
307
+ choices: fc_choices.into_iter().enumerate().map(|(i, (point, quantiles))| Choice {
308
+ index: i,
309
  forecast: ForecastOutput { point, quantiles },
310
  finish_reason: "stop",
311
+ }).collect(),
312
  usage: Usage { context_length, forecast_length },
313
  };
314
 
{src → flowstate-r1-rs/src}/py.rs RENAMED
File without changes
flowstate-r1-rs/uv.lock ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.8"
4
+
5
+ [[package]]
6
+ name = "flowstate-r1-rs"
7
+ version = "0.1.0"
8
+ source = { editable = "." }
9
+
10
+ [package.dev-dependencies]
11
+ dev = [
12
+ { name = "maturin" },
13
+ ]
14
+
15
+ [package.metadata]
16
+
17
+ [package.metadata.requires-dev]
18
+ dev = [{ name = "maturin", specifier = ">=1.14.0" }]
19
+
20
+ [[package]]
21
+ name = "maturin"
22
+ version = "1.14.0"
23
+ source = { registry = "https://pypi.org/simple" }
24
+ dependencies = [
25
+ { name = "tomli", marker = "python_full_version < '3.11'" },
26
+ ]
27
+ sdist = { url = "https://files.pythonhosted.org/packages/a7/d0/b7c8b7778cc44df3efbc96eb23acaa995e06ea1a60eb9b02f29858fcbd08/maturin-1.14.0.tar.gz", hash = "sha256:f7f82a6aca4a6c402bf00b99200be199d4874d04b9b9e74e825726a3478bba7f", size = 367010, upload-time = "2026-06-12T00:13:30.811Z" }
28
+ wheels = [
29
+ { url = "https://files.pythonhosted.org/packages/88/51/49367dcd8f6ec139e69ef0c695c8ff5075223673382101812b4affa53216/maturin-1.14.0-py3-none-linux_armv6l.whl", hash = "sha256:019ea3ec7e71f4c9759a367d4d21022ed5a3a621a2ce123abf3fb114ab3711ca", size = 10204135, upload-time = "2026-06-12T00:13:34.308Z" },
30
+ { url = "https://files.pythonhosted.org/packages/dd/2a/487ce56c838d25e0ce64350e75ec4e3dc89544c0a6233221c229d6aa1a84/maturin-1.14.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6948a10f5f3470b791f79319be51debdd8bfd1778b36f2409f98e1314bc3859b", size = 19736800, upload-time = "2026-06-12T00:13:40.456Z" },
31
+ { url = "https://files.pythonhosted.org/packages/a8/a5/12f2efc18f419edce3282a93629cba16278bb502135dac95cd04ef7c2eae/maturin-1.14.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1506e86b1e273a98074a62e281b13f27ac96f8cdef85f7f98d3e3589a9387a23", size = 10201144, upload-time = "2026-06-12T00:13:26.842Z" },
32
+ { url = "https://files.pythonhosted.org/packages/bf/95/3789e72273fd8bc80c33a11c787634b3251c4989d7a7203a92438836d4ff/maturin-1.14.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:df10ce4f7ba97fd3423f624f39b94c888ae3e5b470642a91918e1ccec81282fd", size = 10182394, upload-time = "2026-06-12T00:13:13.693Z" },
33
+ { url = "https://files.pythonhosted.org/packages/40/79/15957eb4e055597f217e6310963a9c1371372e63c5b4a3e30803365addd2/maturin-1.14.0-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:75bcd4468a7fe597652cc2980c6bb16ce4bb8c411e3eb85dac2c4418cef0e95a", size = 10616603, upload-time = "2026-06-12T00:13:22.795Z" },
34
+ { url = "https://files.pythonhosted.org/packages/3e/4b/d1822f88cd5e855640f0e10ee00c39b9be614c1ef2f827e9792332d94b9f/maturin-1.14.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2d123337e817f8dfe23755d6760139c01104137bb63e9e20c289c547e25ec857", size = 10075309, upload-time = "2026-06-12T00:13:38.274Z" },
35
+ { url = "https://files.pythonhosted.org/packages/c0/82/c1b160d2163e8784489285e82a5c811fdcef3e0704e35b34c1cfe1828de3/maturin-1.14.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:107f84110d890090a01bb1ecd01761fdfae925c23c659ba492c9b83dd179eab4", size = 10024058, upload-time = "2026-06-12T00:13:16.49Z" },
36
+ { url = "https://files.pythonhosted.org/packages/0c/e8/88a9d1872997d4535af10ebe79f550e834880bf613cf8e50b50d2d938e3b/maturin-1.14.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:9a84277aa907961cd47ad26fef1539e79efa30611972eaf7499606e773e991b2", size = 13302073, upload-time = "2026-06-12T00:13:29.027Z" },
37
+ { url = "https://files.pythonhosted.org/packages/4a/13/3f6d28bb7b744558b9bc78c995c1855d7e5ff21ad475f46d9de5c3dab039/maturin-1.14.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:095714b2a904927e3c868a1c5d078257ff0443c5049f7623777352966768306e", size = 10863616, upload-time = "2026-06-12T00:13:32.191Z" },
38
+ { url = "https://files.pythonhosted.org/packages/24/06/39352d2b402efa3a7dd01d4ed197b301ea35eec10208ba2b8c649101f4df/maturin-1.14.0-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:20229d332f87166b930e4ca07cdbee8a1726f2eea87a337610aa25bba3ddf4b4", size = 10399943, upload-time = "2026-06-12T00:13:36.273Z" },
39
+ { url = "https://files.pythonhosted.org/packages/58/77/641504541336240fef3836b2d15a785eaeb33c941fb118513c267dd70840/maturin-1.14.0-py3-none-win32.whl", hash = "sha256:4ba1e3c3f33609f461d587b7549104c81a15fd6d42ba63a73cea9376a1e9876e", size = 8905117, upload-time = "2026-06-12T00:13:18.38Z" },
40
+ { url = "https://files.pythonhosted.org/packages/02/4a/ca247a0c43069b2f48cf783c5b13c3a9eb92c8f596dc7fbdb9f75fea4414/maturin-1.14.0-py3-none-win_amd64.whl", hash = "sha256:cb09a313f097adeb4dda0082277871a28d1bd26615dbadab42e6234b6df6fe69", size = 10309099, upload-time = "2026-06-12T00:13:20.523Z" },
41
+ { url = "https://files.pythonhosted.org/packages/8b/a4/f14a3f6086cc3caaa90d12e832e4aa41de771c310041959f0d35dd4efe17/maturin-1.14.0-py3-none-win_arm64.whl", hash = "sha256:8c1a8188195f5b6ce1aab99ae2d92e342900298f901456b43ca028947fd3b288", size = 9719100, upload-time = "2026-06-12T00:13:24.741Z" },
42
+ ]
43
+
44
+ [[package]]
45
+ name = "tomli"
46
+ version = "2.4.1"
47
+ source = { registry = "https://pypi.org/simple" }
48
+ sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
49
+ wheels = [
50
+ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
51
+ { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
52
+ { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
53
+ { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
54
+ { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
55
+ { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
56
+ { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
57
+ { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
58
+ { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
59
+ { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
60
+ { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
61
+ { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
62
+ { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
63
+ { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
64
+ { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
65
+ { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
66
+ { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
67
+ { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
68
+ { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
69
+ { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
70
+ { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
71
+ { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
72
+ { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
73
+ { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
74
+ { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
75
+ { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
76
+ { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
77
+ { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
78
+ { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
79
+ { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
80
+ { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
81
+ { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
82
+ { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
83
+ { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
84
+ { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
85
+ { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
86
+ { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
87
+ { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
88
+ { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
89
+ { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
90
+ { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
91
+ { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
92
+ { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
93
+ { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
94
+ { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
95
+ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
96
+ ]
pyproject.toml CHANGED
@@ -1,12 +1,12 @@
1
- [build-system]
2
- requires = ["maturin>=1,<2"]
3
- build-backend = "maturin"
4
-
5
  [project]
6
- name = "flowstate-r1-rs"
7
  version = "0.1.0"
8
- requires-python = ">=3.8"
 
 
 
 
 
9
 
10
- [tool.maturin]
11
- features = ["python"]
12
- module-name = "flowstate_r1_rs"
 
 
 
 
 
1
  [project]
2
+ name = "flowstate-r1-env"
3
  version = "0.1.0"
4
+ requires-python = ">=3.11"
5
+ dependencies = [
6
+ "granite-tsfm",
7
+ "numpy",
8
+ "torch",
9
+ ]
10
 
11
+ [tool.uv]
12
+ package = false
 
scripts/compare_python.py DELETED
@@ -1,138 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Numerical comparison: Rust flowstate-r1-rs vs Python FlowStateForPrediction.
4
-
5
- Both engines use the same local safetensors checkpoint (models/).
6
- Input is a fixed 256-step sine+trend series; prediction_length=24.
7
-
8
- Usage:
9
- python3 scripts/compare_python.py \
10
- --model-dir models \
11
- --gguf gguf/flowstate-r1-f32.gguf \
12
- --config models/config.json \
13
- --horizon 24
14
- """
15
-
16
- import argparse
17
- import json
18
- import math
19
- import os
20
- import subprocess
21
- import sys
22
-
23
- import numpy as np
24
-
25
-
26
- def make_test_series(n: int = 256) -> np.ndarray:
27
- t = np.arange(n, dtype=np.float32)
28
- return np.sin(2 * math.pi * t / 48) + 0.02 * t
29
-
30
-
31
- def run_python(model_dir: str, series: np.ndarray, horizon: int) -> np.ndarray:
32
- try:
33
- import torch
34
- from tsfm_public import FlowStateForPrediction
35
- except ImportError:
36
- print("tsfm_public not installed. Install with:")
37
- print(" pip install git+https://github.com/ibm-granite/granite-tsfm.git")
38
- sys.exit(1)
39
-
40
- predictor = FlowStateForPrediction.from_pretrained(model_dir, torch_dtype=torch.float32)
41
- predictor.model.eval()
42
-
43
- with open(os.path.join(model_dir, "config.json")) as f:
44
- cfg = json.load(f)
45
- decoder_patch_len = cfg["decoder_patch_len"]
46
- scale_factor = decoder_patch_len / horizon
47
-
48
- with torch.no_grad():
49
- ts = torch.tensor(series, dtype=torch.float32).unsqueeze(-1).unsqueeze(1)
50
- preds = predictor(
51
- ts,
52
- scale_factor=scale_factor,
53
- prediction_length=horizon,
54
- batch_first=False,
55
- )
56
-
57
- if hasattr(preds, "quantile_outputs") and preds.quantile_outputs is not None:
58
- arr = preds.quantile_outputs[0, :, :, 0].detach().numpy() # [n_q, pred_len]
59
- elif hasattr(preds, "last_hidden_state"):
60
- arr = preds.last_hidden_state.squeeze().detach().numpy()
61
- else:
62
- arr = preds.squeeze().detach().numpy()
63
-
64
- return arr.astype(np.float32)
65
-
66
-
67
- def run_rust(binary: str, gguf: str, config: str, series: np.ndarray,
68
- horizon: int) -> np.ndarray:
69
- request = json.dumps({"context": series.tolist(), "horizon": horizon})
70
- result = subprocess.run(
71
- [binary, "infer", "--gguf", gguf, "--config", config],
72
- input=request, capture_output=True, text=True, check=True,
73
- )
74
- fc = json.loads(result.stdout)["choices"][0]["forecast"]
75
- quants = fc.get("quantiles", {})
76
- keys = sorted(quants.keys(), key=float)
77
- rows = [np.array(quants[k], dtype=np.float32) for k in keys]
78
- if not rows:
79
- return np.array(fc["point"], dtype=np.float32)[None, :]
80
- return np.stack(rows) # (n_quantiles, horizon)
81
-
82
-
83
- def report(py_preds: np.ndarray, rs_preds: np.ndarray, quantile_labels) -> None:
84
- diff = np.abs(py_preds - rs_preds)
85
- print(f"\n{'Quantile':<12} {'MaxAbsErr':>12} {'MeanAbsErr':>12} {'MedianAbsErr':>14}")
86
- print("-" * 54)
87
- for i, q in enumerate(quantile_labels):
88
- d = diff[i]
89
- print(f"{q:<12} {d.max():>12.6f} {d.mean():>12.6f} {np.median(d):>14.6f}")
90
- print("-" * 54)
91
- print(f"{'ALL':12} {diff.max():>12.6f} {diff.mean():>12.6f} {np.median(diff):>14.6f}")
92
-
93
- for idx in [0, len(quantile_labels) // 2, len(quantile_labels) - 1]:
94
- q = quantile_labels[idx]
95
- py = py_preds[idx, :8]
96
- rs = rs_preds[idx, :8]
97
- print(f"\nSample [{q}] first 8 steps:")
98
- print(f" Python: {' '.join(f'{v:.4f}' for v in py)}")
99
- print(f" Rust: {' '.join(f'{v:.4f}' for v in rs)}")
100
- print(f" Diff: {' '.join(f'{abs(a-b):.4f}' for a, b in zip(py, rs))}")
101
-
102
-
103
- def main():
104
- parser = argparse.ArgumentParser()
105
- parser.add_argument("--model-dir", default="models")
106
- parser.add_argument("--gguf", default="gguf/flowstate-r1-f32.gguf")
107
- parser.add_argument("--config", default="models/config.json")
108
- parser.add_argument("--horizon", type=int, default=24)
109
- parser.add_argument("--binary", default="./target/release/flowstate-r1-rs")
110
- args = parser.parse_args()
111
-
112
- with open(args.config) as f:
113
- cfg = json.load(f)
114
- quantiles = cfg["quantiles"]
115
- quantile_labels = [f"q{q:.2f}" for q in quantiles]
116
-
117
- series = make_test_series(256)
118
-
119
- print(f"Input: {len(series)}-step sine+trend | Horizon: {args.horizon}")
120
- print(f"Quantiles: {quantile_labels}")
121
-
122
- print("\n--- Running Python (HuggingFace safetensors) ---")
123
- py_preds = run_python(args.model_dir, series, args.horizon)
124
- print(f"Python output shape: {py_preds.shape}")
125
-
126
- print("\n--- Running Rust (GGUF) ---")
127
- rs_preds = run_rust(args.binary, args.gguf, args.config, series, args.horizon)
128
- print(f"Rust output shape: {rs_preds.shape}")
129
-
130
- if py_preds.shape != rs_preds.shape:
131
- print(f"Shape mismatch: Python {py_preds.shape} vs Rust {rs_preds.shape}")
132
- sys.exit(1)
133
-
134
- report(py_preds, rs_preds, quantile_labels)
135
-
136
-
137
- if __name__ == "__main__":
138
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/convert_all.sh DELETED
@@ -1,30 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Download ibm-granite/granite-timeseries-flowstate-r1 and convert to GGUF in all dtypes.
3
- set -euo pipefail
4
- cd "$(dirname "$0")/.."
5
-
6
- BIN=./target/release/flowstate-r1-rs
7
- MODEL=${1:-ibm-granite/granite-timeseries-flowstate-r1}
8
- MODEL_DIR=models
9
- TOKEN_ARG=""
10
- if [[ -n "${HF_TOKEN:-}" ]]; then
11
- TOKEN_ARG="--token $HF_TOKEN"
12
- fi
13
-
14
- if [[ ! -x "$BIN" ]]; then
15
- echo "Binary not found — run: cargo build --release"
16
- exit 1
17
- fi
18
-
19
- mkdir -p gguf
20
-
21
- for DTYPE in f32 f16 q8; do
22
- OUT="gguf/flowstate-r1-${DTYPE}.gguf"
23
- echo "=== Converting ${MODEL} → ${OUT} (dtype=${DTYPE}) ==="
24
- $BIN convert --model "$MODEL" --dtype "$DTYPE" \
25
- --model-dir "$MODEL_DIR" --output "$OUT" $TOKEN_ARG
26
- ls -lh "$OUT"
27
- done
28
-
29
- echo "Done. Files:"
30
- ls -lh gguf/flowstate-r1-*.gguf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/upload.sh DELETED
@@ -1,20 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Upload source + GGUF files to HuggingFace Hub via the built-in upload subcommand.
3
- #
4
- # Usage:
5
- # HF_TOKEN=hf_... ./scripts/upload.sh [--repo owner/repo-name]
6
- #
7
- set -euo pipefail
8
- cd "$(dirname "$0")/.."
9
-
10
- BIN=./target/release/flowstate-r1-rs
11
-
12
- if [[ ! -f "$BIN" ]]; then
13
- echo "Binary not found — building release …"
14
- cargo build --release
15
- fi
16
-
17
- exec "$BIN" upload \
18
- --repo "amaye15/flowstate-r1-gguf" \
19
- ${HF_TOKEN:+--token "$HF_TOKEN"} \
20
- "$@"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/config.rs DELETED
@@ -1,51 +0,0 @@
1
- use serde::Deserialize;
2
-
3
- /// Top-level FlowState `config.json`.
4
- #[derive(Debug, Deserialize, Clone)]
5
- pub struct FlowStateConfig {
6
- pub context_length: u32,
7
- pub decoder_dim: u32,
8
- pub decoder_patch_len: u32,
9
- pub decoder_type: String,
10
- pub embedding_feature_dim: u32,
11
- pub encoder_num_hippo_blocks: u32,
12
- pub encoder_num_layers: u32,
13
- pub encoder_state_dim: u32,
14
- pub quantiles: Vec<f32>,
15
- #[serde(default = "default_bool_true")]
16
- pub with_missing: bool,
17
- #[serde(default = "default_bool_true")]
18
- pub use_freq: bool,
19
- #[serde(default = "default_bool_true")]
20
- pub init_processing: bool,
21
- #[serde(default = "default_u32_2048")]
22
- pub min_context: u32,
23
- }
24
-
25
- impl FlowStateConfig {
26
- pub fn from_json(s: &str) -> anyhow::Result<Self> {
27
- Ok(serde_json::from_str(s)?)
28
- }
29
-
30
- pub fn n_quantiles(&self) -> u32 {
31
- self.quantiles.len() as u32
32
- }
33
-
34
- /// Number of input channels (value + missing mask).
35
- pub fn n_inputs(&self) -> u32 {
36
- if self.with_missing { 2 } else { 1 }
37
- }
38
-
39
- /// Legendre basis range for "legs" / "hlegs" decoder.
40
- pub fn basis_range(&self) -> [f32; 2] {
41
- let dt = self.decoder_type.to_lowercase();
42
- if dt == "hlegs" {
43
- [0.0, 0.95]
44
- } else {
45
- [-1.0, 0.95]
46
- }
47
- }
48
- }
49
-
50
- fn default_bool_true() -> bool { true }
51
- fn default_u32_2048() -> u32 { 2048 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/convert.rs DELETED
@@ -1,265 +0,0 @@
1
- use std::fs::File;
2
- use std::io::BufWriter;
3
- use std::path::Path;
4
-
5
- use anyhow::Context;
6
- use indicatif::{ProgressBar, ProgressStyle};
7
- use safetensors::SafeTensors;
8
- use safetensors::Dtype as StDtype;
9
-
10
- use crate::config::FlowStateConfig;
11
- use crate::download::ModelFiles;
12
- use crate::gguf::{GGMLType, GGUFMetaValue, GGUFWriter};
13
- use crate::tensor_map::map_tensor_name;
14
-
15
- pub struct ConvertOptions {
16
- pub output_dtype: GGMLType,
17
- }
18
-
19
- pub fn convert(
20
- model_id: &str,
21
- files: &ModelFiles,
22
- config: &FlowStateConfig,
23
- opts: &ConvertOptions,
24
- output_path: &Path,
25
- ) -> anyhow::Result<()> {
26
- let mut writer = GGUFWriter::new();
27
- write_metadata(&mut writer, model_id, config);
28
-
29
- let shard_bytes = load_shard_bytes(&files.safetensors_shards)?;
30
- let shard_views: Vec<SafeTensors> = shard_bytes
31
- .iter()
32
- .map(|b| SafeTensors::deserialize(b).context("deserialize shard"))
33
- .collect::<anyhow::Result<_>>()?;
34
-
35
- let total_tensors: usize = shard_views.iter().map(|s| s.len()).sum();
36
- println!("Found {} tensors across {} shard(s).", total_tensors, shard_views.len());
37
-
38
- let pb = ProgressBar::new(total_tensors as u64);
39
- pb.set_style(
40
- ProgressStyle::with_template(
41
- "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}",
42
- )
43
- .unwrap()
44
- .progress_chars("=>-"),
45
- );
46
-
47
- let mut mapped = 0usize;
48
- let mut skipped: Vec<String> = Vec::new();
49
- let mut fallback_count = 0usize;
50
-
51
- for shard in &shard_views {
52
- for (hf_name, tensor_view) in shard.tensors() {
53
- pb.set_message(hf_name.to_string());
54
-
55
- let gguf_name = match map_tensor_name(&hf_name) {
56
- Some(n) => n,
57
- None => {
58
- skipped.push(hf_name.to_string());
59
- pb.inc(1);
60
- continue;
61
- }
62
- };
63
-
64
- let src_dtype = ggml_type_from_st(tensor_view.dtype())
65
- .with_context(|| format!("tensor {hf_name}: unsupported dtype {:?}", tensor_view.dtype()))?;
66
-
67
- let raw_data = tensor_view.data();
68
- let py_shape = tensor_view.shape();
69
- let n_elems: usize = py_shape.iter().product();
70
- let innermost = py_shape.last().copied().unwrap_or(1);
71
-
72
- let (dst_dtype, gguf_shape, tensor_data) =
73
- if opts.output_dtype == GGMLType::Q8_0 && (innermost % 32 != 0 || n_elems % 32 != 0) {
74
- fallback_count += 1;
75
- let data = cast_data(raw_data, src_dtype, GGMLType::F32)
76
- .with_context(|| format!("tensor {hf_name}: cast failed"))?;
77
- let gs = py_shape.iter().rev().map(|&d| d as u64).collect();
78
- (GGMLType::F32, gs, data)
79
- } else {
80
- let dst = opts.output_dtype;
81
- let data = cast_data(raw_data, src_dtype, dst)
82
- .with_context(|| format!("tensor {hf_name}: cast failed"))?;
83
- let gs = py_shape.iter().rev().map(|&d| d as u64).collect();
84
- (dst, gs, data)
85
- };
86
-
87
- writer.add_tensor(gguf_name, gguf_shape, dst_dtype, tensor_data);
88
- mapped += 1;
89
- pb.inc(1);
90
- }
91
- }
92
-
93
- pb.finish_with_message("tensors processed");
94
-
95
- if !skipped.is_empty() {
96
- eprintln!("\nWarning: {} tensor(s) skipped (not mapped):", skipped.len());
97
- for name in &skipped {
98
- eprintln!(" {name}");
99
- }
100
- }
101
- if fallback_count > 0 {
102
- eprintln!("\nNote: {fallback_count} tensor(s) fell back to F32 (too small for Q8_0 blocks).");
103
- }
104
-
105
- println!("Writing {mapped} tensors to {} …", output_path.display());
106
- let out_file = File::create(output_path)
107
- .with_context(|| format!("create {}", output_path.display()))?;
108
- let mut buf_writer = BufWriter::new(out_file);
109
- writer.write_to(&mut buf_writer)?;
110
- println!("Done.");
111
- Ok(())
112
- }
113
-
114
- fn write_metadata(writer: &mut GGUFWriter, model_id: &str, config: &FlowStateConfig) {
115
- writer.add_metadata("general.architecture", GGUFMetaValue::String("flowstate".into()));
116
- writer.add_metadata("general.name", GGUFMetaValue::String(model_id.into()));
117
-
118
- writer.add_metadata("flowstate.block_count", GGUFMetaValue::Uint32(config.encoder_num_layers));
119
- writer.add_metadata("flowstate.embedding_length", GGUFMetaValue::Uint32(config.embedding_feature_dim));
120
- writer.add_metadata("flowstate.state_dim", GGUFMetaValue::Uint32(config.encoder_state_dim));
121
- writer.add_metadata("flowstate.num_hippo_blocks", GGUFMetaValue::Uint32(config.encoder_num_hippo_blocks));
122
- writer.add_metadata("flowstate.context_length", GGUFMetaValue::Uint32(config.context_length));
123
- writer.add_metadata("flowstate.min_context", GGUFMetaValue::Uint32(config.min_context));
124
- writer.add_metadata("flowstate.decoder_dim", GGUFMetaValue::Uint32(config.decoder_dim));
125
- writer.add_metadata("flowstate.decoder_patch_len", GGUFMetaValue::Uint32(config.decoder_patch_len));
126
- writer.add_metadata("flowstate.decoder_type", GGUFMetaValue::String(config.decoder_type.clone()));
127
- writer.add_metadata("flowstate.quantile_count", GGUFMetaValue::Uint32(config.n_quantiles()));
128
- writer.add_metadata("flowstate.quantiles", GGUFMetaValue::ArrayFloat32(config.quantiles.clone()));
129
- writer.add_metadata("flowstate.with_missing", GGUFMetaValue::Bool(config.with_missing));
130
-
131
- let range = config.basis_range();
132
- writer.add_metadata("flowstate.basis_range_low", GGUFMetaValue::Float32(range[0]));
133
- writer.add_metadata("flowstate.basis_range_high", GGUFMetaValue::Float32(range[1]));
134
- }
135
-
136
- fn ggml_type_from_st(dtype: StDtype) -> anyhow::Result<GGMLType> {
137
- match dtype {
138
- StDtype::F32 => Ok(GGMLType::F32),
139
- StDtype::F16 => Ok(GGMLType::F16),
140
- StDtype::BF16 => Ok(GGMLType::BF16),
141
- other => anyhow::bail!("unsupported safetensors dtype: {other:?}"),
142
- }
143
- }
144
-
145
- fn load_shard_bytes(shards: &[std::path::PathBuf]) -> anyhow::Result<Vec<Vec<u8>>> {
146
- shards
147
- .iter()
148
- .map(|p| std::fs::read(p).with_context(|| format!("read shard {}", p.display())))
149
- .collect()
150
- }
151
-
152
- fn cast_data(data: &[u8], src: GGMLType, dst: GGMLType) -> anyhow::Result<Vec<u8>> {
153
- if src == dst { return Ok(data.to_vec()); }
154
- if dst == GGMLType::Q8_0 {
155
- let f32_values = decode_to_f32(data, src)?;
156
- return quantize_q8_0(&f32_values);
157
- }
158
- match (src, dst) {
159
- (GGMLType::F32, GGMLType::F16) => {
160
- let f32_values = parse_f32_le(data)?;
161
- let mut out = Vec::with_capacity(f32_values.len() * 2);
162
- for v in f32_values { let bits = f32_to_f16_bits(v); out.extend_from_slice(&bits.to_le_bytes()); }
163
- Ok(out)
164
- }
165
- (GGMLType::BF16, GGMLType::F32) => {
166
- let mut out = Vec::with_capacity(data.len() * 2);
167
- for chunk in data.chunks_exact(2) {
168
- let bf16_bits = u16::from_le_bytes([chunk[0], chunk[1]]);
169
- out.extend_from_slice(&((bf16_bits as u32) << 16).to_le_bytes());
170
- }
171
- Ok(out)
172
- }
173
- (GGMLType::BF16, GGMLType::F16) => {
174
- let mut out = Vec::with_capacity(data.len());
175
- for chunk in data.chunks_exact(2) {
176
- let bf16_bits = u16::from_le_bytes([chunk[0], chunk[1]]);
177
- let f32_val = f32::from_bits((bf16_bits as u32) << 16);
178
- out.extend_from_slice(&f32_to_f16_bits(f32_val).to_le_bytes());
179
- }
180
- Ok(out)
181
- }
182
- (GGMLType::F16, GGMLType::F32) => {
183
- let mut out = Vec::with_capacity(data.len() * 2);
184
- for chunk in data.chunks_exact(2) {
185
- let f16_bits = u16::from_le_bytes([chunk[0], chunk[1]]);
186
- out.extend_from_slice(&f16_to_f32(f16_bits).to_bits().to_le_bytes());
187
- }
188
- Ok(out)
189
- }
190
- _ => anyhow::bail!("unsupported cast: {src:?} → {dst:?}"),
191
- }
192
- }
193
-
194
- fn decode_to_f32(data: &[u8], src: GGMLType) -> anyhow::Result<Vec<f32>> {
195
- match src {
196
- GGMLType::F32 => parse_f32_le(data),
197
- GGMLType::F16 => data.chunks_exact(2).map(|c| Ok(f16_to_f32(u16::from_le_bytes([c[0], c[1]])))).collect(),
198
- GGMLType::BF16 => data.chunks_exact(2).map(|c| {
199
- let bits = u16::from_le_bytes([c[0], c[1]]);
200
- Ok(f32::from_bits((bits as u32) << 16))
201
- }).collect(),
202
- GGMLType::Q8_0 => anyhow::bail!("Q8_0 → Q8_0 re-quantization not supported"),
203
- }
204
- }
205
-
206
- fn quantize_q8_0(values: &[f32]) -> anyhow::Result<Vec<u8>> {
207
- const BLOCK: usize = 32;
208
- if values.len() % BLOCK != 0 {
209
- anyhow::bail!("Q8_0 requires element count divisible by {BLOCK}, got {}", values.len());
210
- }
211
- let n_blocks = values.len() / BLOCK;
212
- let mut out = vec![0u8; n_blocks * 34];
213
- for b in 0..n_blocks {
214
- let blk = &values[b * BLOCK..(b + 1) * BLOCK];
215
- let amax = blk.iter().copied().map(f32::abs).fold(0.0f32, f32::max);
216
- let d = if amax == 0.0 { 0.0f32 } else { amax / 127.0 };
217
- let d_inv = if d == 0.0 { 0.0f32 } else { 1.0 / d };
218
- let base = b * 34;
219
- out[base..base + 2].copy_from_slice(&f32_to_f16_bits(d).to_le_bytes());
220
- for i in 0..BLOCK {
221
- out[base + 2 + i] = (blk[i] * d_inv).round().clamp(-127.0, 127.0) as i8 as u8;
222
- }
223
- }
224
- Ok(out)
225
- }
226
-
227
- fn parse_f32_le(data: &[u8]) -> anyhow::Result<Vec<f32>> {
228
- if data.len() % 4 != 0 { anyhow::bail!("f32 data length not divisible by 4"); }
229
- Ok(data.chunks_exact(4).map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])).collect())
230
- }
231
-
232
- fn f32_to_f16_bits(v: f32) -> u16 {
233
- let bits = v.to_bits();
234
- let sign = ((bits >> 16) & 0x8000) as u16;
235
- let exp = ((bits >> 23) & 0xFF) as i32;
236
- let mantissa = bits & 0x007F_FFFF;
237
- if exp == 0xFF { return sign | 0x7C00 | if mantissa != 0 { 0x0200 } else { 0 }; }
238
- let new_exp = exp - 127 + 15;
239
- if new_exp >= 31 { return sign | 0x7C00; }
240
- if new_exp <= 0 {
241
- if new_exp < -10 { return sign; }
242
- let m = (mantissa | 0x0080_0000) >> (1 - new_exp);
243
- return sign | (m >> 13) as u16;
244
- }
245
- sign | ((new_exp as u16) << 10) | (mantissa >> 13) as u16
246
- }
247
-
248
- fn f16_to_f32(bits: u16) -> f32 {
249
- let sign = ((bits & 0x8000) as u32) << 16;
250
- let exp = ((bits >> 10) & 0x1F) as i32;
251
- let mantissa = (bits & 0x03FF) as u32;
252
- let f32_bits = if exp == 0 {
253
- if mantissa == 0 { sign }
254
- else {
255
- let mut m = mantissa; let mut e = 0i32;
256
- while m & 0x0400 == 0 { m <<= 1; e += 1; }
257
- sign | ((127 - 15 - e + 1) as u32) << 23 | (m & 0x03FF) << 13
258
- }
259
- } else if exp == 31 {
260
- sign | 0x7F80_0000 | (mantissa << 13)
261
- } else {
262
- sign | ((exp + 127 - 15) as u32) << 23 | (mantissa << 13)
263
- };
264
- f32::from_bits(f32_bits)
265
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/download.rs DELETED
@@ -1,182 +0,0 @@
1
- use std::path::{Path, PathBuf};
2
-
3
- use anyhow::Context;
4
- use futures_util::StreamExt;
5
- use serde::Deserialize;
6
- use tokio::io::AsyncWriteExt;
7
-
8
- const HF_BASE: &str = "https://huggingface.co";
9
-
10
- /// Paths to all local files needed for conversion.
11
- pub struct ModelFiles {
12
- pub config_json: PathBuf,
13
- /// Ordered list of safetensors shard paths, already downloaded locally.
14
- pub safetensors_shards: Vec<PathBuf>,
15
- }
16
-
17
- /// Download (or locate from cache) all model files for `repo_id` into `model_dir`.
18
- pub async fn download_model(
19
- repo_id: &str,
20
- hf_token: Option<&str>,
21
- model_dir: &Path,
22
- ) -> anyhow::Result<ModelFiles> {
23
- let client = build_client(hf_token)?;
24
- std::fs::create_dir_all(model_dir).context("create model dir")?;
25
-
26
- println!("Fetching config.json …");
27
- let config_json = fetch_file(&client, repo_id, "config.json", model_dir).await?;
28
-
29
- // Detect sharded model by fetching the index file.
30
- let shards =
31
- match fetch_file(&client, repo_id, "model.safetensors.index.json", model_dir).await {
32
- Ok(index_path) => {
33
- println!("Found sharded model — reading index …");
34
- resolve_shards(&client, repo_id, &index_path, model_dir).await?
35
- }
36
- Err(_) => {
37
- println!("Fetching model.safetensors …");
38
- let shard =
39
- fetch_file(&client, repo_id, "model.safetensors", model_dir).await?;
40
- vec![shard]
41
- }
42
- };
43
-
44
- Ok(ModelFiles {
45
- config_json,
46
- safetensors_shards: shards,
47
- })
48
- }
49
-
50
- fn build_client(hf_token: Option<&str>) -> anyhow::Result<reqwest::Client> {
51
- let mut headers = reqwest::header::HeaderMap::new();
52
- headers.insert(
53
- reqwest::header::USER_AGENT,
54
- "chronos-rs/0.1".parse().unwrap(),
55
- );
56
- if let Some(token) = hf_token {
57
- headers.insert(
58
- reqwest::header::AUTHORIZATION,
59
- format!("Bearer {token}").parse().context("invalid HF token")?,
60
- );
61
- }
62
- Ok(reqwest::Client::builder()
63
- .default_headers(headers)
64
- .redirect(reqwest::redirect::Policy::limited(10))
65
- .build()?)
66
- }
67
-
68
- /// Download `filename` from `repo_id` into `dest_dir`, resuming if a partial
69
- /// `.tmp` file already exists. Returns the local path of the completed file.
70
- async fn fetch_file(
71
- client: &reqwest::Client,
72
- repo_id: &str,
73
- filename: &str,
74
- dest_dir: &Path,
75
- ) -> anyhow::Result<PathBuf> {
76
- let dest = dest_dir.join(filename.replace('/', "_"));
77
- if dest.exists() {
78
- println!(" (cached) {filename}");
79
- return Ok(dest);
80
- }
81
-
82
- let dest_tmp = dest.with_extension("tmp");
83
- let already = if dest_tmp.exists() {
84
- dest_tmp.metadata()?.len()
85
- } else {
86
- 0
87
- };
88
-
89
- let url = format!("{HF_BASE}/{repo_id}/resolve/main/{filename}");
90
-
91
- let mut req = client.get(&url);
92
- if already > 0 {
93
- req = req.header(reqwest::header::RANGE, format!("bytes={already}-"));
94
- println!(" Resuming {filename} from {} MB …", already / 1_000_000);
95
- }
96
-
97
- let response = req
98
- .send()
99
- .await
100
- .with_context(|| format!("GET {url}"))?;
101
-
102
- let status = response.status();
103
- if !status.is_success() {
104
- anyhow::bail!("HTTP {status} fetching {filename} from {repo_id}");
105
- }
106
-
107
- let (file, resume_offset) = if status == reqwest::StatusCode::PARTIAL_CONTENT {
108
- let f = tokio::fs::OpenOptions::new()
109
- .append(true)
110
- .open(&dest_tmp)
111
- .await
112
- .with_context(|| format!("open tmp {}", dest_tmp.display()))?;
113
- (f, already)
114
- } else {
115
- let f = tokio::fs::File::create(&dest_tmp)
116
- .await
117
- .with_context(|| format!("create tmp {}", dest_tmp.display()))?;
118
- (f, 0)
119
- };
120
-
121
- let total = response
122
- .content_length()
123
- .map(|n| n + resume_offset)
124
- .unwrap_or(0);
125
-
126
- let pb = indicatif::ProgressBar::new(total);
127
- pb.set_style(
128
- indicatif::ProgressStyle::with_template(
129
- " {msg} [{bar:40}] {bytes}/{total_bytes} ({bytes_per_sec}, eta {eta})",
130
- )
131
- .unwrap()
132
- .progress_chars("=>-"),
133
- );
134
- pb.set_message(filename.to_string());
135
- pb.set_position(resume_offset);
136
-
137
- {
138
- let mut file = file;
139
- let mut stream = response.bytes_stream();
140
- while let Some(chunk) = stream.next().await {
141
- let chunk = chunk.with_context(|| format!("stream chunk of {filename}"))?;
142
- pb.inc(chunk.len() as u64);
143
- file.write_all(&chunk)
144
- .await
145
- .with_context(|| format!("write chunk to {}", dest_tmp.display()))?;
146
- }
147
- }
148
-
149
- pb.finish_and_clear();
150
- std::fs::rename(&dest_tmp, &dest)
151
- .with_context(|| format!("rename tmp → {}", dest.display()))?;
152
-
153
- Ok(dest)
154
- }
155
-
156
- /// Parse the shard index JSON and download every unique shard.
157
- async fn resolve_shards(
158
- client: &reqwest::Client,
159
- repo_id: &str,
160
- index_path: &Path,
161
- cache_dir: &Path,
162
- ) -> anyhow::Result<Vec<PathBuf>> {
163
- #[derive(Deserialize)]
164
- struct Index {
165
- weight_map: std::collections::HashMap<String, String>,
166
- }
167
-
168
- let raw = std::fs::read_to_string(index_path).context("read index json")?;
169
- let index: Index = serde_json::from_str(&raw).context("parse index json")?;
170
-
171
- let mut shard_names: Vec<String> = index.weight_map.into_values().collect();
172
- shard_names.sort();
173
- shard_names.dedup();
174
-
175
- let mut paths = Vec::with_capacity(shard_names.len());
176
- for name in &shard_names {
177
- println!(" Fetching {name} …");
178
- let p = fetch_file(client, repo_id, name, cache_dir).await?;
179
- paths.push(p);
180
- }
181
- Ok(paths)
182
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/gguf/mod.rs DELETED
@@ -1,5 +0,0 @@
1
- pub mod types;
2
- pub mod writer;
3
-
4
- pub use types::{GGMLType, GGUFMetaValue};
5
- pub use writer::GGUFWriter;
 
 
 
 
 
 
src/gguf/types.rs DELETED
@@ -1,86 +0,0 @@
1
- /// GGML tensor data types used in GGUF files.
2
- /// Values match the ggml_type enum in ggml.h.
3
- #[derive(Debug, Clone, Copy, PartialEq, Eq)]
4
- #[repr(u32)]
5
- pub enum GGMLType {
6
- F32 = 0,
7
- F16 = 1,
8
- /// Q8_0: blocks of 32 × i8 with a shared f16 scale (34 bytes/block).
9
- Q8_0 = 8,
10
- BF16 = 30,
11
- }
12
-
13
- impl GGMLType {
14
- /// (block_elems, bytes_per_block) for block-quantized types; None for float types.
15
- /// Q8_0: 32 × i8 values + 1 × f16 scale = 34 bytes/block.
16
- #[allow(dead_code)]
17
- pub fn block_shape(self) -> Option<(usize, usize)> {
18
- match self {
19
- GGMLType::Q8_0 => Some((32, 34)),
20
- _ => None,
21
- }
22
- }
23
- }
24
-
25
- /// GGUF metadata value types (gguf_metadata_value_type).
26
- #[derive(Debug, Clone, Copy, PartialEq, Eq)]
27
- #[repr(u32)]
28
- #[allow(dead_code)]
29
- pub enum GGUFValueType {
30
- Uint8 = 0,
31
- Int8 = 1,
32
- Uint16 = 2,
33
- Int16 = 3,
34
- Uint32 = 4,
35
- Int32 = 5,
36
- Float32 = 6,
37
- Bool = 7,
38
- String = 8,
39
- Array = 9,
40
- Uint64 = 10,
41
- Int64 = 11,
42
- Float64 = 12,
43
- }
44
-
45
- /// A typed metadata value for a GGUF key-value pair.
46
- #[derive(Debug, Clone)]
47
- #[allow(dead_code)]
48
- pub enum GGUFMetaValue {
49
- Uint8(u8),
50
- Int8(i8),
51
- Uint16(u16),
52
- Int16(i16),
53
- Uint32(u32),
54
- Int32(i32),
55
- Float32(f32),
56
- Bool(bool),
57
- String(String),
58
- Uint64(u64),
59
- Int64(i64),
60
- Float64(f64),
61
- ArrayUint32(Vec<u32>),
62
- ArrayString(Vec<String>),
63
- ArrayFloat32(Vec<f32>),
64
- }
65
-
66
- impl GGUFMetaValue {
67
- pub fn value_type(&self) -> GGUFValueType {
68
- match self {
69
- GGUFMetaValue::Uint8(_) => GGUFValueType::Uint8,
70
- GGUFMetaValue::Int8(_) => GGUFValueType::Int8,
71
- GGUFMetaValue::Uint16(_) => GGUFValueType::Uint16,
72
- GGUFMetaValue::Int16(_) => GGUFValueType::Int16,
73
- GGUFMetaValue::Uint32(_) => GGUFValueType::Uint32,
74
- GGUFMetaValue::Int32(_) => GGUFValueType::Int32,
75
- GGUFMetaValue::Float32(_) => GGUFValueType::Float32,
76
- GGUFMetaValue::Bool(_) => GGUFValueType::Bool,
77
- GGUFMetaValue::String(_) => GGUFValueType::String,
78
- GGUFMetaValue::Uint64(_) => GGUFValueType::Uint64,
79
- GGUFMetaValue::Int64(_) => GGUFValueType::Int64,
80
- GGUFMetaValue::Float64(_) => GGUFValueType::Float64,
81
- GGUFMetaValue::ArrayUint32(_)
82
- | GGUFMetaValue::ArrayString(_)
83
- | GGUFMetaValue::ArrayFloat32(_) => GGUFValueType::Array,
84
- }
85
- }
86
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/gguf/writer.rs DELETED
@@ -1,161 +0,0 @@
1
- use std::io::{self, Seek, Write};
2
-
3
- use byteorder::{LittleEndian, WriteBytesExt};
4
-
5
- use super::types::{GGMLType, GGUFMetaValue, GGUFValueType};
6
-
7
- const GGUF_MAGIC: &[u8; 4] = b"GGUF";
8
- const GGUF_VERSION: u32 = 3;
9
- const ALIGNMENT: u64 = 32;
10
-
11
- /// Describes one tensor's position in the GGUF data section.
12
- #[derive(Debug)]
13
- struct TensorInfo {
14
- name: String,
15
- shape: Vec<u64>,
16
- dtype: GGMLType,
17
- /// Byte offset from the start of the tensor data block.
18
- offset: u64,
19
- /// Raw tensor bytes (row-major, little-endian).
20
- data: Vec<u8>,
21
- }
22
-
23
- /// Streaming GGUF v3 writer.
24
- ///
25
- /// Call [`add_metadata`] for every key-value pair, then [`add_tensor`] for
26
- /// every tensor, then [`write_to`] to flush the complete file.
27
- pub struct GGUFWriter {
28
- metadata: Vec<(String, GGUFMetaValue)>,
29
- tensors: Vec<TensorInfo>,
30
- /// Running data-block offset for the next tensor.
31
- data_offset: u64,
32
- }
33
-
34
- impl GGUFWriter {
35
- pub fn new() -> Self {
36
- Self {
37
- metadata: Vec::new(),
38
- tensors: Vec::new(),
39
- data_offset: 0,
40
- }
41
- }
42
-
43
- pub fn add_metadata(&mut self, key: impl Into<String>, value: GGUFMetaValue) {
44
- self.metadata.push((key.into(), value));
45
- }
46
-
47
- /// Buffer a tensor. `data` must already be in the target dtype byte layout.
48
- pub fn add_tensor(
49
- &mut self,
50
- name: impl Into<String>,
51
- shape: Vec<u64>,
52
- dtype: GGMLType,
53
- data: Vec<u8>,
54
- ) {
55
- let offset = self.data_offset;
56
- let padded_len = round_up(data.len() as u64, ALIGNMENT);
57
- self.data_offset += padded_len;
58
- self.tensors.push(TensorInfo {
59
- name: name.into(),
60
- shape,
61
- dtype,
62
- offset,
63
- data,
64
- });
65
- }
66
-
67
- /// Serialize the complete GGUF file to `writer`.
68
- pub fn write_to<W: Write + Seek>(&self, writer: &mut W) -> anyhow::Result<()> {
69
- // --- header ---
70
- writer.write_all(GGUF_MAGIC)?;
71
- writer.write_u32::<LittleEndian>(GGUF_VERSION)?;
72
- writer.write_u64::<LittleEndian>(self.tensors.len() as u64)?;
73
- writer.write_u64::<LittleEndian>(self.metadata.len() as u64)?;
74
-
75
- // --- metadata key-value pairs ---
76
- for (key, value) in &self.metadata {
77
- write_string(writer, key)?;
78
- writer.write_u32::<LittleEndian>(value.value_type() as u32)?;
79
- write_value(writer, value)?;
80
- }
81
-
82
- // --- tensor info ---
83
- for t in &self.tensors {
84
- write_string(writer, &t.name)?;
85
- writer.write_u32::<LittleEndian>(t.shape.len() as u32)?;
86
- for &dim in &t.shape {
87
- writer.write_u64::<LittleEndian>(dim)?;
88
- }
89
- writer.write_u32::<LittleEndian>(t.dtype as u32)?;
90
- writer.write_u64::<LittleEndian>(t.offset)?;
91
- }
92
-
93
- // --- align to ALIGNMENT before tensor data ---
94
- let pos = writer.stream_position()?;
95
- let aligned = round_up(pos, ALIGNMENT);
96
- if aligned > pos {
97
- let pad = vec![0u8; (aligned - pos) as usize];
98
- writer.write_all(&pad)?;
99
- }
100
-
101
- // --- tensor data (each padded to ALIGNMENT) ---
102
- for t in &self.tensors {
103
- writer.write_all(&t.data)?;
104
- let remainder = t.data.len() as u64 % ALIGNMENT;
105
- if remainder != 0 {
106
- let pad = vec![0u8; (ALIGNMENT - remainder) as usize];
107
- writer.write_all(&pad)?;
108
- }
109
- }
110
-
111
- Ok(())
112
- }
113
- }
114
-
115
- fn round_up(value: u64, align: u64) -> u64 {
116
- (value + align - 1) / align * align
117
- }
118
-
119
- fn write_string<W: Write>(writer: &mut W, s: &str) -> io::Result<()> {
120
- writer.write_u64::<LittleEndian>(s.len() as u64)?;
121
- writer.write_all(s.as_bytes())
122
- }
123
-
124
- fn write_value<W: Write>(writer: &mut W, value: &GGUFMetaValue) -> anyhow::Result<()> {
125
- match value {
126
- GGUFMetaValue::Uint8(v) => writer.write_u8(*v)?,
127
- GGUFMetaValue::Int8(v) => writer.write_i8(*v)?,
128
- GGUFMetaValue::Uint16(v) => writer.write_u16::<LittleEndian>(*v)?,
129
- GGUFMetaValue::Int16(v) => writer.write_i16::<LittleEndian>(*v)?,
130
- GGUFMetaValue::Uint32(v) => writer.write_u32::<LittleEndian>(*v)?,
131
- GGUFMetaValue::Int32(v) => writer.write_i32::<LittleEndian>(*v)?,
132
- GGUFMetaValue::Float32(v) => writer.write_f32::<LittleEndian>(*v)?,
133
- GGUFMetaValue::Bool(v) => writer.write_u8(*v as u8)?,
134
- GGUFMetaValue::String(v) => write_string(writer, v)?,
135
- GGUFMetaValue::Uint64(v) => writer.write_u64::<LittleEndian>(*v)?,
136
- GGUFMetaValue::Int64(v) => writer.write_i64::<LittleEndian>(*v)?,
137
- GGUFMetaValue::Float64(v) => writer.write_f64::<LittleEndian>(*v)?,
138
- GGUFMetaValue::ArrayUint32(arr) => {
139
- writer.write_u32::<LittleEndian>(GGUFValueType::Uint32 as u32)?;
140
- writer.write_u64::<LittleEndian>(arr.len() as u64)?;
141
- for v in arr {
142
- writer.write_u32::<LittleEndian>(*v)?;
143
- }
144
- }
145
- GGUFMetaValue::ArrayString(arr) => {
146
- writer.write_u32::<LittleEndian>(GGUFValueType::String as u32)?;
147
- writer.write_u64::<LittleEndian>(arr.len() as u64)?;
148
- for s in arr {
149
- write_string(writer, s)?;
150
- }
151
- }
152
- GGUFMetaValue::ArrayFloat32(arr) => {
153
- writer.write_u32::<LittleEndian>(GGUFValueType::Float32 as u32)?;
154
- writer.write_u64::<LittleEndian>(arr.len() as u64)?;
155
- for v in arr {
156
- writer.write_f32::<LittleEndian>(*v)?;
157
- }
158
- }
159
- }
160
- Ok(())
161
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/infer/mod.rs DELETED
@@ -1,557 +0,0 @@
1
- use std::path::Path;
2
-
3
- use anyhow::Context;
4
- use candle_core::{DType, Device, Tensor};
5
- use candle_core::quantized::gguf_file;
6
-
7
- // ---------------------------------------------------------------------------
8
- // Config
9
- // ---------------------------------------------------------------------------
10
-
11
- #[derive(Debug, Clone)]
12
- pub struct InferConfig {
13
- pub num_layers: usize,
14
- pub embed_dim: usize,
15
- pub state_dim: usize,
16
- pub n_inputs: usize, // 2 for with_missing (value + mask)
17
- pub decoder_dim: usize,
18
- pub decoder_patch_len: usize,
19
- pub quantiles: Vec<f32>,
20
- pub basis_range: [f32; 2], // e.g. [-1.0, 0.95] for "legs"
21
- pub context_length: usize,
22
- pub eps: f32,
23
- }
24
-
25
- // ---------------------------------------------------------------------------
26
- // Weight structs
27
- // ---------------------------------------------------------------------------
28
-
29
- struct S5Weights {
30
- log_lambda_real: Vec<f32>, // [state_dim]
31
- lambda_imag: Vec<f32>, // [state_dim]
32
- b_r: Tensor, // [state_dim, embed_dim] (row-major, HF order)
33
- b_i: Tensor, // [state_dim, embed_dim]
34
- c_r: Tensor, // [embed_dim, state_dim] (Python C_tilde shape)
35
- c_i: Tensor, // [embed_dim, state_dim]
36
- d: Vec<f32>, // [embed_dim]
37
- log_delta: Vec<f32>, // [state_dim]
38
- }
39
-
40
- struct BlockWeights {
41
- ssm: S5Weights,
42
- out_weight: Tensor, // [embed_dim, embed_dim]
43
- out_bias: Vec<f32>, // [embed_dim]
44
- norm_weight: Vec<f32>, // [embed_dim]
45
- norm_bias: Vec<f32>, // [embed_dim]
46
- }
47
-
48
- pub struct FlowStateModel {
49
- device: Device,
50
- config: InferConfig,
51
- embed_w: Tensor, // [embed_dim, n_inputs]
52
- embed_b: Vec<f32>, // [embed_dim]
53
- blocks: Vec<BlockWeights>,
54
- decoder_w: Tensor, // [n_quantiles * decoder_dim, embed_dim]
55
- decoder_b: Vec<f32>, // [n_quantiles * decoder_dim]
56
- }
57
-
58
- // ---------------------------------------------------------------------------
59
- // Loading
60
- // ---------------------------------------------------------------------------
61
-
62
- fn load_f32_vec(
63
- content: &gguf_file::Content,
64
- reader: &mut (impl std::io::Read + std::io::Seek),
65
- name: &str,
66
- device: &Device,
67
- ) -> anyhow::Result<Vec<f32>> {
68
- let t = content.tensor(reader, name, device)
69
- .with_context(|| format!("missing tensor: {name}"))?;
70
- let t = t.dequantize(device)?;
71
- let t = t.to_dtype(DType::F32)?;
72
- Ok(t.flatten_all()?.to_vec1()?)
73
- }
74
-
75
- fn load_matrix(
76
- content: &gguf_file::Content,
77
- reader: &mut (impl std::io::Read + std::io::Seek),
78
- name: &str,
79
- device: &Device,
80
- ) -> anyhow::Result<Tensor> {
81
- let t = content.tensor(reader, name, device)
82
- .with_context(|| format!("missing tensor: {name}"))?;
83
- let t = t.dequantize(device)?;
84
- Ok(t.to_dtype(DType::F32)?)
85
- }
86
-
87
- impl FlowStateModel {
88
- pub fn load(gguf_path: &Path, config: InferConfig) -> anyhow::Result<Self> {
89
- let device = Device::Cpu;
90
- let mut file = std::fs::File::open(gguf_path)
91
- .with_context(|| format!("open {}", gguf_path.display()))?;
92
- let content = gguf_file::Content::read(&mut file).context("read GGUF header")?;
93
-
94
- // Embedding
95
- let embed_w = load_matrix(&content, &mut file, "embed.weight", &device)?;
96
- let embed_b = load_f32_vec(&content, &mut file, "embed.bias", &device)?;
97
-
98
- // Encoder blocks
99
- let mut blocks = Vec::with_capacity(config.num_layers);
100
- for n in 0..config.num_layers {
101
- let ssm = S5Weights {
102
- log_lambda_real: load_f32_vec(&content, &mut file, &format!("blk.{n}.ssm.log_lambda_real"), &device)?,
103
- lambda_imag: load_f32_vec(&content, &mut file, &format!("blk.{n}.ssm.lambda_imag"), &device)?,
104
- b_r: load_matrix(&content, &mut file, &format!("blk.{n}.ssm.b_r"), &device)?,
105
- b_i: load_matrix(&content, &mut file, &format!("blk.{n}.ssm.b_i"), &device)?,
106
- c_r: load_matrix(&content, &mut file, &format!("blk.{n}.ssm.c_r"), &device)?,
107
- c_i: load_matrix(&content, &mut file, &format!("blk.{n}.ssm.c_i"), &device)?,
108
- d: load_f32_vec(&content, &mut file, &format!("blk.{n}.ssm.d"), &device)?,
109
- log_delta: load_f32_vec(&content, &mut file, &format!("blk.{n}.ssm.log_delta"), &device)?,
110
- };
111
- blocks.push(BlockWeights {
112
- ssm,
113
- out_weight: load_matrix(&content, &mut file, &format!("blk.{n}.out.weight"), &device)?,
114
- out_bias: load_f32_vec(&content, &mut file, &format!("blk.{n}.out.bias"), &device)?,
115
- norm_weight: load_f32_vec(&content, &mut file, &format!("blk.{n}.norm.weight"), &device)?,
116
- norm_bias: load_f32_vec(&content, &mut file, &format!("blk.{n}.norm.bias"), &device)?,
117
- });
118
- }
119
-
120
- // Decoder
121
- let decoder_w = load_matrix(&content, &mut file, "decoder.weight", &device)?;
122
- let decoder_b = load_f32_vec(&content, &mut file, "decoder.bias", &device)?;
123
-
124
- Ok(Self { device, config, embed_w, embed_b, blocks, decoder_w, decoder_b })
125
- }
126
-
127
- // -----------------------------------------------------------------------
128
- // Public inference entry point
129
- // -----------------------------------------------------------------------
130
-
131
- /// Forecast `prediction_length` steps from a univariate context series.
132
- /// Returns `Vec<Vec<f32>>` of shape `[n_quantiles][prediction_length]`.
133
- pub fn forecast(&self, context: &[f32], prediction_length: usize) -> anyhow::Result<Vec<Vec<f32>>> {
134
- let cfg = &self.config;
135
-
136
- // 1. Pad or trim context to match model requirements
137
- let ctx_len = context.len().min(cfg.context_length);
138
- let start = context.len().saturating_sub(ctx_len);
139
- let context = &context[start..];
140
- let seq_len = context.len();
141
-
142
- // 2. Causal RevIN: compute prefix statistics
143
- let (normed_values, final_mean, final_std) = causal_revin_norm(context, cfg.eps);
144
- if std::env::var("FLOWSTATE_DEBUG").is_ok() {
145
- eprintln!("RevIN final_mean={:.8} final_std={:.8}", final_mean, final_std);
146
- eprintln!("normed[0..4]: {:.6} {:.6} {:.6} {:.6}",
147
- normed_values[0], normed_values[1], normed_values[2], normed_values[3]);
148
- eprintln!("normed[252..256]: {:.6} {:.6} {:.6} {:.6}",
149
- normed_values[252], normed_values[253], normed_values[254], normed_values[255]);
150
- }
151
-
152
- // 3. Build input tensor [seq_len, n_inputs] with mask channel = 0 (no missing)
153
- let mut input_data = vec![0.0f32; seq_len * cfg.n_inputs];
154
- for t in 0..seq_len {
155
- input_data[t * cfg.n_inputs] = normed_values[t];
156
- if cfg.n_inputs > 1 {
157
- input_data[t * cfg.n_inputs + 1] = 0.0; // mask = 0 means known
158
- }
159
- }
160
-
161
- // 4. Embedding: [seq_len, n_inputs] × embed_w^T + embed_b → [seq_len, embed_dim]
162
- let input_t = Tensor::from_vec(input_data, (seq_len, cfg.n_inputs), &self.device)?;
163
- let mut hidden = linear(&input_t, &self.embed_w, &self.embed_b, &self.device)?;
164
- // hidden: [seq_len, embed_dim]
165
-
166
- // 5. Scale factor for discretization: decoder_patch_len / prediction_length
167
- let scale_factor = cfg.decoder_patch_len as f32 / prediction_length as f32;
168
-
169
- // 6. Encoder: 6 S5 layers
170
- let num_layers = self.blocks.len();
171
- for (i, block) in self.blocks.iter().enumerate() {
172
- let is_last = i == num_layers - 1;
173
- hidden = self.apply_s5_layer(hidden, block, scale_factor, is_last, seq_len)?;
174
- }
175
- // After last layer: hidden is [1, embed_dim]
176
-
177
- // 7. Decoder: linear → [1, n_quantiles * decoder_dim]
178
- let coeffs = linear(&hidden, &self.decoder_w, &self.decoder_b, &self.device)?;
179
- let n_q = cfg.quantiles.len();
180
- let coeffs_data: Vec<f32> = coeffs.flatten_all()?.to_vec1()?;
181
- // coeffs_data: [n_quantiles * decoder_dim]
182
-
183
- // Diagnostic: dump all coefficients when FLOWSTATE_DEBUG is set
184
- if std::env::var("FLOWSTATE_DEBUG").is_ok() {
185
- // Print all coefficients as CSV lines to stderr: COEFF,{qi},{d},{val}
186
- for qi in 0..n_q {
187
- for d in 0..cfg.decoder_dim {
188
- eprintln!("COEFF,{qi},{d},{:.8}", coeffs_data[qi * cfg.decoder_dim + d]);
189
- }
190
- }
191
- }
192
-
193
- // 8. Compute Legendre basis [prediction_length, decoder_dim]
194
- // Python: t[i] = range[0] + i * scale * (range[1]-range[0]) / decoder_patch_len (i=1..n)
195
- let basis = legendre_basis(prediction_length, cfg.decoder_dim, cfg.basis_range,
196
- scale_factor, cfg.decoder_patch_len);
197
- // basis: [prediction_length][decoder_dim], scaled by 1/4
198
-
199
- // 9. Matrix multiply: [n_q, decoder_dim] × [prediction_length, decoder_dim]^T
200
- // = [n_q, prediction_length]
201
- let mut output = vec![vec![0.0f32; prediction_length]; n_q];
202
- for q in 0..n_q {
203
- let coeff_offset = q * cfg.decoder_dim;
204
- for p in 0..prediction_length {
205
- let mut sum = 0.0f32;
206
- for d in 0..cfg.decoder_dim {
207
- sum += coeffs_data[coeff_offset + d] * basis[p][d];
208
- }
209
- output[q][p] = sum;
210
- }
211
- }
212
-
213
- // 10. Denormalize: output * final_std + final_mean
214
- for q in 0..n_q {
215
- for p in 0..prediction_length {
216
- output[q][p] = output[q][p] * final_std + final_mean;
217
- }
218
- }
219
-
220
- Ok(output)
221
- }
222
-
223
- // -----------------------------------------------------------------------
224
- // S5 layer (one encoder block)
225
- // -----------------------------------------------------------------------
226
-
227
- fn apply_s5_layer(
228
- &self,
229
- x: Tensor, // [seq_len, embed_dim] for non-last; [seq_len, embed_dim] → output [1, embed_dim] for last
230
- block: &BlockWeights,
231
- scale_factor: f32,
232
- is_last: bool,
233
- _seq_len: usize,
234
- ) -> anyhow::Result<Tensor> {
235
- let cfg = &self.config;
236
- let state_dim = cfg.state_dim;
237
- let embed_dim = cfg.embed_dim;
238
-
239
- let seq_len = x.dim(0)?;
240
-
241
- // Save skip connection (trimmed for last layer)
242
- let skip = if is_last {
243
- x.narrow(0, seq_len - 1, 1)? // last row: [1, embed_dim]
244
- } else {
245
- x.clone()
246
- };
247
-
248
- // ---- SSM forward ----
249
-
250
- // Discretize: compute A_bar and B_bar from SSM parameters
251
- let ssm = &block.ssm;
252
- let (a_bar_r, a_bar_i, b_bar_r_data, b_bar_i_data) =
253
- discretize(ssm, scale_factor, state_dim, embed_dim);
254
-
255
- // B @ x for all timesteps at once: x [seq_len, embed_dim] × B^T [embed_dim, state_dim]
256
- // B_tilde stored as [state_dim, embed_dim], so B @ x = x @ B^T
257
- let b_bar_r_t = Tensor::from_vec(b_bar_r_data, (state_dim, embed_dim), &self.device)?;
258
- let b_bar_i_t = Tensor::from_vec(b_bar_i_data, (state_dim, embed_dim), &self.device)?;
259
-
260
- // x @ B^T → [seq_len, state_dim]
261
- let bu_r = x.matmul(&b_bar_r_t.t()?)?;
262
- let bu_i = x.matmul(&b_bar_i_t.t()?)?;
263
- let bu_r_data: Vec<f32> = bu_r.flatten_all()?.to_vec1()?;
264
- let bu_i_data: Vec<f32> = bu_i.flatten_all()?.to_vec1()?;
265
-
266
- // Sequential SSM scan
267
- let mut h_r = vec![0.0f32; state_dim]; // real part of hidden state
268
- let mut h_i = vec![0.0f32; state_dim]; // imag part
269
- let mut all_h_r = vec![0.0f32; seq_len * state_dim];
270
- let mut all_h_i = vec![0.0f32; seq_len * state_dim];
271
-
272
- for t in 0..seq_len {
273
- for s in 0..state_dim {
274
- let new_r = a_bar_r[s] * h_r[s] - a_bar_i[s] * h_i[s] + bu_r_data[t * state_dim + s];
275
- let new_i = a_bar_r[s] * h_i[s] + a_bar_i[s] * h_r[s] + bu_i_data[t * state_dim + s];
276
- h_r[s] = new_r;
277
- h_i[s] = new_i;
278
- }
279
- all_h_r[t * state_dim..(t + 1) * state_dim].copy_from_slice(&h_r);
280
- all_h_i[t * state_dim..(t + 1) * state_dim].copy_from_slice(&h_i);
281
- }
282
-
283
- // For last layer, keep only the final timestep
284
- let (h_r_t, h_i_t, out_seq_len) = if is_last {
285
- let hr = Tensor::from_vec(all_h_r[(seq_len - 1) * state_dim..].to_vec(), (1, state_dim), &self.device)?;
286
- let hi = Tensor::from_vec(all_h_i[(seq_len - 1) * state_dim..].to_vec(), (1, state_dim), &self.device)?;
287
- (hr, hi, 1usize)
288
- } else {
289
- let hr = Tensor::from_vec(all_h_r, (seq_len, state_dim), &self.device)?;
290
- let hi = Tensor::from_vec(all_h_i, (seq_len, state_dim), &self.device)?;
291
- (hr, hi, seq_len)
292
- };
293
-
294
- // C @ h: y_real = C_r @ h_r - C_i @ h_i
295
- // C_tilde_r stored as [embed_dim, state_dim]; h_r as [out_seq_len, state_dim]
296
- // y = h @ C^T → [out_seq_len, embed_dim]
297
- let y_from_cr = h_r_t.matmul(&block.ssm.c_r.t()?)?; // [out_seq_len, embed_dim]
298
- let y_from_ci = h_i_t.matmul(&block.ssm.c_i.t()?)?; // [out_seq_len, embed_dim]
299
- let mut y_data: Vec<f32> = (y_from_cr - y_from_ci)?.flatten_all()?.to_vec1()?;
300
-
301
- // Add D skip: y += D * x_at_positions
302
- let x_positions = if is_last {
303
- x.narrow(0, seq_len - 1, 1)?.flatten_all()?.to_vec1::<f32>()?
304
- } else {
305
- x.flatten_all()?.to_vec1::<f32>()?
306
- };
307
- let d = &block.ssm.d;
308
- for t in 0..out_seq_len {
309
- for e in 0..embed_dim {
310
- y_data[t * embed_dim + e] += d[e] * x_positions[t * embed_dim + e];
311
- }
312
- }
313
-
314
- // ---- MLP: selu(y) * sigmoid(out_linear(selu(y))) ----
315
- let y_t = Tensor::from_vec(y_data, (out_seq_len, embed_dim), &self.device)?;
316
- let y_selu = selu_tensor(&y_t)?;
317
- let gate_pre = linear(&y_selu, &block.out_weight, &block.out_bias, &self.device)?;
318
- let gate = sigmoid_tensor(&gate_pre)?;
319
- let y_gated = y_selu.mul(&gate)?;
320
-
321
- // ---- LayerNorm ----
322
- let y_normed = layer_norm(&y_gated, &block.norm_weight, &block.norm_bias, self.config.eps)?;
323
-
324
- // ---- Residual ----
325
- let out = (y_normed + skip)?;
326
-
327
- Ok(out)
328
- }
329
- }
330
-
331
- // ---------------------------------------------------------------------------
332
- // Causal RevIN
333
- // ---------------------------------------------------------------------------
334
-
335
- /// Returns (normalized_values[seq_len], final_mean, final_std).
336
- /// Each position t is normalized by the cumulative mean/std of x[0..=t].
337
- fn causal_revin_norm(x: &[f32], eps: f32) -> (Vec<f32>, f32, f32) {
338
- let n = x.len();
339
- let mut normed = vec![0.0f32; n];
340
- let mut cum_sum = 0.0f32;
341
- let mut cum_sq_diff = 0.0f32;
342
- let mut final_mean = 0.0f32;
343
- let mut final_std = 1.0f32;
344
-
345
- for t in 0..n {
346
- let count = (t + 1) as f32;
347
- cum_sum += x[t];
348
- let mean_t = cum_sum / count;
349
-
350
- // Python: cumsum((x[s] - mean_s)^2) / count — each step uses its own running mean
351
- cum_sq_diff += (x[t] - mean_t) * (x[t] - mean_t);
352
- let var_t = (cum_sq_diff / count).max(0.0);
353
- let std_t = (var_t + eps).sqrt();
354
-
355
- normed[t] = (x[t] - mean_t) / std_t;
356
-
357
- if t == n - 1 {
358
- final_mean = mean_t;
359
- final_std = std_t;
360
- }
361
- }
362
-
363
- (normed, final_mean, final_std)
364
- }
365
-
366
- // ---------------------------------------------------------------------------
367
- // SSM discretization
368
- // ---------------------------------------------------------------------------
369
-
370
- /// Returns (A_bar_real, A_bar_imag, B_bar_real_flat, B_bar_imag_flat).
371
- /// B_bar shape: [state_dim, embed_dim] flattened row-major.
372
- fn discretize(
373
- ssm: &S5Weights,
374
- scale_factor: f32,
375
- state_dim: usize,
376
- embed_dim: usize,
377
- ) -> (Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>) {
378
- // lambda = -exp(log_lambda_real) + j * lambda_imag
379
- // delta = exp(scale_factor * log_delta)
380
- // A_bar = exp(lambda * delta) = exp((lambda_r + j*lambda_i) * delta)
381
- // = exp(lambda_r * delta) * (cos(lambda_i * delta) + j*sin(lambda_i * delta))
382
-
383
- let mut a_r = vec![0.0f32; state_dim];
384
- let mut a_i = vec![0.0f32; state_dim];
385
-
386
- // (A_bar - 1) / lambda per element (complex division)
387
- let mut coeff_r = vec![0.0f32; state_dim];
388
- let mut coeff_i = vec![0.0f32; state_dim];
389
-
390
- for s in 0..state_dim {
391
- let lam_r = -ssm.log_lambda_real[s].exp();
392
- let lam_i = ssm.lambda_imag[s];
393
- let delta = (scale_factor * ssm.log_delta[s]).exp();
394
-
395
- // A_bar = exp(lambda * delta)
396
- let exp_r = lam_r * delta;
397
- let exp_i = lam_i * delta;
398
- let mag = exp_r.exp();
399
- a_r[s] = mag * exp_i.cos();
400
- a_i[s] = mag * exp_i.sin();
401
-
402
- // (A_bar - 1) / lambda
403
- // numerator: (a_r - 1) + j*a_i
404
- // denominator: lam_r + j*lam_i
405
- let num_r = a_r[s] - 1.0;
406
- let num_i = a_i[s];
407
- let denom_sq = lam_r * lam_r + lam_i * lam_i;
408
- if denom_sq > 1e-20 {
409
- coeff_r[s] = (num_r * lam_r + num_i * lam_i) / denom_sq;
410
- coeff_i[s] = (num_i * lam_r - num_r * lam_i) / denom_sq;
411
- } else {
412
- coeff_r[s] = delta; // limit as lambda → 0
413
- coeff_i[s] = 0.0;
414
- }
415
- }
416
-
417
- // B_bar = coeff * B_tilde (element-wise complex multiply, then multiply B matrix)
418
- // B_tilde stored as [state_dim, embed_dim] in GGUF (transposed from Python HF which is also [state_dim, emb])
419
- // After load_matrix, candle reverses dims: stored [embed_dim, state_dim] in GGUF → loaded as [state_dim, embed_dim]
420
- // Actually GGUF reversal: Python [state_dim, embed_dim] → GGUF stores reversed → candle sees [embed_dim, state_dim]
421
- // But we treat it as [state_dim, embed_dim] for the einsum B @ u = u @ B^T
422
- // Let's get B_tilde data and compute B_bar manually
423
- // The GGUF shape is reversed from Python. Python B_tilde: [512, 512].
424
- // So GGUF/candle sees it as [512, 512] (square, so same either way).
425
-
426
- // Get B_tilde data as flat arrays [state_dim * embed_dim]
427
- // b_r and b_i are loaded as candle tensors; we need them as flat vecs for the per-state multiply
428
- // They were stored with reversed shape but 512×512 is symmetric in shape
429
- // We extract row by row: B_tilde[s, :] = row s of the [state_dim, embed_dim] matrix
430
-
431
- // Use flat data from the tensors
432
- let b_r_data = get_tensor_data_row_major(&ssm.b_r, state_dim, embed_dim);
433
- let b_i_data = get_tensor_data_row_major(&ssm.b_i, state_dim, embed_dim);
434
-
435
- // B_bar[s, e] = coeff_r[s]*b_r[s,e] - coeff_i[s]*b_i[s,e] (real)
436
- // + coeff_r[s]*b_i[s,e] + coeff_i[s]*b_r[s,e] (imag)
437
- let mut b_bar_r = vec![0.0f32; state_dim * embed_dim];
438
- let mut b_bar_i = vec![0.0f32; state_dim * embed_dim];
439
-
440
- for s in 0..state_dim {
441
- for e in 0..embed_dim {
442
- let br = b_r_data[s * embed_dim + e];
443
- let bi = b_i_data[s * embed_dim + e];
444
- b_bar_r[s * embed_dim + e] = coeff_r[s] * br - coeff_i[s] * bi;
445
- b_bar_i[s * embed_dim + e] = coeff_r[s] * bi + coeff_i[s] * br;
446
- }
447
- }
448
-
449
- (a_r, a_i, b_bar_r, b_bar_i)
450
- }
451
-
452
- /// Extract tensor data in row-major order as Vec<f32>.
453
- /// Handles candle's internal memory layout for F32 tensors.
454
- fn get_tensor_data_row_major(t: &Tensor, rows: usize, cols: usize) -> Vec<f32> {
455
- t.to_dtype(DType::F32)
456
- .and_then(|t| t.reshape((rows, cols)))
457
- .and_then(|t| t.flatten_all())
458
- .and_then(|t| t.to_vec1())
459
- .unwrap_or_else(|_| vec![0.0f32; rows * cols])
460
- }
461
-
462
- // ---------------------------------------------------------------------------
463
- // Legendre basis (FlowStateLegendreBasis equivalent)
464
- // ---------------------------------------------------------------------------
465
-
466
- /// Public wrapper for diagnostics.
467
- pub fn dump_legendre_basis(n_points: usize, degree: usize, range: [f32; 2],
468
- scale: f32, pred_dist: usize) -> Vec<Vec<f32>> {
469
- legendre_basis(n_points, degree, range, scale, pred_dist)
470
- }
471
-
472
- /// Compute Legendre polynomial basis matrix.
473
- /// Returns [n_points][degree+1] scaled by 1/4 (as in get_kernel).
474
- /// Python formula: t[i] = range[0] + i * scale * (range[1]-range[0]) / pred_dist (i=1..n_points)
475
- fn legendre_basis(n_points: usize, degree: usize, range: [f32; 2],
476
- scale: f32, pred_dist: usize) -> Vec<Vec<f32>> {
477
- let dt = scale * (range[1] - range[0]) / pred_dist as f32;
478
- let t: Vec<f32> = (1..=n_points).map(|i| range[0] + i as f32 * dt).collect();
479
-
480
- // Evaluate Legendre polynomials at each t: [n_points, degree+1]
481
- // P_0 = 1, P_1 = x, P_k = ((2k-1)*x*P_{k-1} - (k-1)*P_{k-2}) / k
482
- let mut basis = vec![vec![0.0f32; degree + 1]; n_points];
483
- for (p, &x) in t.iter().enumerate() {
484
- basis[p][0] = 1.0;
485
- if degree >= 1 {
486
- basis[p][1] = x;
487
- }
488
- for k in 1..degree {
489
- let kf = k as f32;
490
- basis[p][k + 1] =
491
- ((2.0 * kf + 1.0) * x * basis[p][k] - kf * basis[p][k - 1]) / (kf + 1.0);
492
- }
493
- // Scale by 1/4 (from get_kernel: return f.float() / 4.0)
494
- for d in 0..=degree {
495
- basis[p][d] /= 4.0;
496
- }
497
- }
498
-
499
- basis
500
- }
501
-
502
- // ---------------------------------------------------------------------------
503
- // Neural network primitives
504
- // ---------------------------------------------------------------------------
505
-
506
- /// y = x @ w^T + b (w: [out, in])
507
- fn linear(x: &Tensor, w: &Tensor, b: &[f32], device: &Device) -> anyhow::Result<Tensor> {
508
- let out_size = b.len();
509
- let y = x.matmul(&w.t()?)?;
510
- let b_t = Tensor::from_vec(b.to_vec(), (1, out_size), device)?.broadcast_as(y.shape())?;
511
- Ok((y + b_t)?)
512
- }
513
-
514
- /// SELU activation: selu(x) = scale * (x if x>0 else alpha*(exp(x)-1))
515
- fn selu_tensor(x: &Tensor) -> anyhow::Result<Tensor> {
516
- const SCALE: f64 = 1.0507009873554804934193349852946;
517
- const ALPHA: f64 = 1.6732632423543772848170429916717;
518
- let data: Vec<f32> = x.flatten_all()?.to_vec1()?;
519
- let selu_data: Vec<f32> = data.iter().map(|&v| {
520
- if v > 0.0 {
521
- (SCALE * v as f64) as f32
522
- } else {
523
- (SCALE * ALPHA * (v.exp() as f64 - 1.0)) as f32
524
- }
525
- }).collect();
526
- Ok(Tensor::from_vec(selu_data, x.shape(), x.device())?)
527
- }
528
-
529
- /// Sigmoid: 1 / (1 + exp(-x))
530
- fn sigmoid_tensor(x: &Tensor) -> anyhow::Result<Tensor> {
531
- let data: Vec<f32> = x.flatten_all()?.to_vec1()?;
532
- let sig_data: Vec<f32> = data.iter().map(|&v| 1.0 / (1.0 + (-v).exp())).collect();
533
- Ok(Tensor::from_vec(sig_data, x.shape(), x.device())?)
534
- }
535
-
536
- /// LayerNorm: (x - mean) / std * weight + bias (over last dimension)
537
- fn layer_norm(x: &Tensor, weight: &[f32], bias: &[f32], eps: f32) -> anyhow::Result<Tensor> {
538
- let shape = x.shape().clone();
539
- let last_dim = *shape.dims().last().context("empty tensor")?;
540
- let n = shape.elem_count();
541
- let n_rows = n / last_dim;
542
-
543
- let data: Vec<f32> = x.flatten_all()?.to_vec1()?;
544
- let mut out = vec![0.0f32; n];
545
-
546
- for r in 0..n_rows {
547
- let row = &data[r * last_dim..(r + 1) * last_dim];
548
- let mean: f32 = row.iter().sum::<f32>() / last_dim as f32;
549
- let var: f32 = row.iter().map(|&v| (v - mean).powi(2)).sum::<f32>() / last_dim as f32;
550
- let std_inv = 1.0 / (var + eps).sqrt();
551
- for i in 0..last_dim {
552
- out[r * last_dim + i] = (row[i] - mean) * std_inv * weight[i] + bias[i];
553
- }
554
- }
555
-
556
- Ok(Tensor::from_vec(out, shape, x.device())?)
557
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/main.rs DELETED
@@ -1,316 +0,0 @@
1
- mod config;
2
- mod convert;
3
- mod download;
4
- mod gguf;
5
- mod infer;
6
- mod tensor_map;
7
- mod upload;
8
-
9
- use std::collections::BTreeMap;
10
- use std::path::PathBuf;
11
- use std::time::{SystemTime, UNIX_EPOCH};
12
-
13
- use anyhow::Context;
14
- use clap::{Parser, Subcommand, ValueEnum};
15
- use serde::Serialize;
16
-
17
- use config::FlowStateConfig;
18
- use convert::{convert, ConvertOptions};
19
- use download::download_model;
20
- use gguf::GGMLType;
21
- use infer::{FlowStateModel, InferConfig};
22
-
23
- #[derive(Parser)]
24
- #[command(name = "flowstate-r1-rs", about = "Convert and run ibm-granite/granite-timeseries-flowstate-r1")]
25
- struct Cli {
26
- #[command(subcommand)]
27
- command: Command,
28
- }
29
-
30
- #[derive(Subcommand)]
31
- enum Command {
32
- /// Upload source + GGUF files to HuggingFace Hub.
33
- Upload {
34
- /// HuggingFace repo to upload to (owner/name).
35
- #[arg(short, long, default_value = "amaye15/flowstate-r1-gguf")]
36
- repo: String,
37
- /// HuggingFace API token (or set HF_TOKEN env var).
38
- #[arg(long, env = "HF_TOKEN")]
39
- token: String,
40
- },
41
- /// Download a FlowState model from HuggingFace and convert it to GGUF.
42
- Convert {
43
- #[arg(short, long, default_value = "ibm-granite/granite-timeseries-flowstate-r1")]
44
- model: String,
45
- #[arg(short, long, default_value = "gguf/flowstate-r1-f16.gguf")]
46
- output: PathBuf,
47
- #[arg(long, default_value = "f16")]
48
- dtype: DtypeArg,
49
- #[arg(long, default_value = "models")]
50
- model_dir: PathBuf,
51
- #[arg(long, env = "HF_TOKEN")]
52
- token: Option<String>,
53
- },
54
- /// Print all tensor names in a local safetensors or GGUF file.
55
- InspectTensors {
56
- path: PathBuf,
57
- },
58
- /// Dump the Legendre basis matrix to CSV for comparison with Python numpy.
59
- DumpBasis {
60
- #[arg(long, default_value = "models/config.json")]
61
- config: PathBuf,
62
- #[arg(short, long, default_value = "24")]
63
- prediction_length: usize,
64
- },
65
- /// Run FlowState univariate forecasting from a GGUF file.
66
- ///
67
- /// Reads a JSON request from stdin: {"context": [...], "horizon": N}
68
- /// Outputs a JSON forecast in OpenAI-compatible format with all quantile levels.
69
- /// The `point` field contains the median quantile (q0.5).
70
- Infer {
71
- /// Path to the GGUF file.
72
- #[arg(short, long, default_value = "gguf/flowstate-r1-f16.gguf")]
73
- gguf: PathBuf,
74
-
75
- /// Path to config.json from the original HuggingFace model.
76
- #[arg(long, default_value = "models/config.json")]
77
- config: PathBuf,
78
- },
79
- }
80
-
81
- #[derive(Clone, ValueEnum)]
82
- enum DtypeArg {
83
- F32,
84
- F16,
85
- Q8,
86
- }
87
-
88
- impl From<DtypeArg> for GGMLType {
89
- fn from(d: DtypeArg) -> Self {
90
- match d {
91
- DtypeArg::F32 => GGMLType::F32,
92
- DtypeArg::F16 => GGMLType::F16,
93
- DtypeArg::Q8 => GGMLType::Q8_0,
94
- }
95
- }
96
- }
97
-
98
- #[tokio::main]
99
- async fn main() -> anyhow::Result<()> {
100
- let cli = Cli::parse();
101
-
102
- match cli.command {
103
- Command::Upload { repo, token } => {
104
- let crate_root = std::env::current_dir().context("current dir")?;
105
- let model_root = crate_root.parent()
106
- .map(|p| p.to_path_buf())
107
- .unwrap_or(crate_root);
108
- upload::run(&repo, &token, &model_root).await?;
109
- }
110
-
111
- Command::Convert { model, output, dtype, model_dir, token } => {
112
- println!("Downloading {model} into {} …", model_dir.display());
113
- let files = download_model(&model, token.as_deref(), &model_dir)
114
- .await
115
- .context("download failed")?;
116
-
117
- let config_str = std::fs::read_to_string(&files.config_json)
118
- .context("read config.json")?;
119
- let config = FlowStateConfig::from_json(&config_str)
120
- .context("parse config.json")?;
121
-
122
- println!(
123
- "Config: {} encoder layers, embed_dim={}, state_dim={}, decoder_dim={}, {} quantiles",
124
- config.encoder_num_layers,
125
- config.embedding_feature_dim,
126
- config.encoder_state_dim,
127
- config.decoder_dim,
128
- config.n_quantiles(),
129
- );
130
-
131
- let opts = ConvertOptions { output_dtype: dtype.into() };
132
- convert(&model, &files, &config, &opts, &output)?;
133
- println!("Wrote {}", output.display());
134
- }
135
-
136
- Command::InspectTensors { path } => {
137
- let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
138
- if ext == "gguf" {
139
- let mut file = std::fs::File::open(&path)
140
- .with_context(|| format!("open {}", path.display()))?;
141
- let content = candle_core::quantized::gguf_file::Content::read(&mut file)
142
- .context("read GGUF")?;
143
- println!("Tensors in {}:", path.display());
144
- let mut names: Vec<_> = content.tensor_infos.keys().cloned().collect();
145
- names.sort();
146
- for name in names {
147
- let ti = &content.tensor_infos[&name];
148
- println!(" {name:80} {:?} {:?}", ti.ggml_dtype, ti.shape);
149
- }
150
- } else {
151
- let bytes = std::fs::read(&path)
152
- .with_context(|| format!("read {}", path.display()))?;
153
- let tensors = safetensors::SafeTensors::deserialize(&bytes)
154
- .context("deserialize safetensors")?;
155
- println!("Tensors in {}:", path.display());
156
- let mut names: Vec<_> = tensors.names().into_iter().collect();
157
- names.sort();
158
- for name in names {
159
- let t = tensors.tensor(name).unwrap();
160
- println!(" {name:80} {:?} {:?}", t.dtype(), t.shape());
161
- }
162
- }
163
- }
164
-
165
- Command::DumpBasis { config, prediction_length } => {
166
- let config_str = std::fs::read_to_string(&config)
167
- .with_context(|| format!("read {}", config.display()))?;
168
- let fs_config = FlowStateConfig::from_json(&config_str)
169
- .context("parse config.json")?;
170
- let decoder_dim = fs_config.decoder_dim as usize;
171
- let basis_range = fs_config.basis_range();
172
- let scale = fs_config.decoder_patch_len as f32 / prediction_length as f32;
173
- let basis = infer::dump_legendre_basis(prediction_length, decoder_dim, basis_range, scale, fs_config.decoder_patch_len as usize);
174
- let header: Vec<String> = (0..decoder_dim).map(|d| format!("P{d}")).collect();
175
- println!("{}", header.join(","));
176
- for row in &basis {
177
- let vals: Vec<String> = row[..decoder_dim].iter().map(|v| format!("{v:.8}")).collect();
178
- println!("{}", vals.join(","));
179
- }
180
- }
181
-
182
- Command::Infer { gguf, config } => {
183
- use std::io::Read;
184
- let mut buf = String::new();
185
- std::io::stdin().read_to_string(&mut buf).context("read stdin")?;
186
- let req: serde_json::Value = serde_json::from_str(&buf).context("parse JSON input")?;
187
- let contexts = parse_contexts(req["context"].clone())?;
188
- let horizon: usize = req["horizon"].as_u64().context("horizon must be a positive integer")? as usize;
189
-
190
- let config_str = std::fs::read_to_string(&config)
191
- .with_context(|| format!("read {}", config.display()))?;
192
- let fs_config = FlowStateConfig::from_json(&config_str)
193
- .context("parse config.json")?;
194
-
195
- let infer_config = InferConfig {
196
- num_layers: fs_config.encoder_num_layers as usize,
197
- embed_dim: fs_config.embedding_feature_dim as usize,
198
- state_dim: fs_config.encoder_state_dim as usize,
199
- n_inputs: fs_config.n_inputs() as usize,
200
- decoder_dim: fs_config.decoder_dim as usize,
201
- decoder_patch_len: fs_config.decoder_patch_len as usize,
202
- quantiles: fs_config.quantiles.clone(),
203
- basis_range: fs_config.basis_range(),
204
- context_length: fs_config.context_length as usize,
205
- eps: 1e-5,
206
- };
207
-
208
- let quantile_levels = infer_config.quantiles.clone();
209
- let median_idx = quantile_levels
210
- .iter()
211
- .position(|&q| (q - 0.5).abs() < 1e-6)
212
- .unwrap_or(quantile_levels.len() / 2);
213
-
214
- eprintln!("Loading model from {} …", gguf.display());
215
- let model = FlowStateModel::load(&gguf, infer_config)
216
- .context("load model")?;
217
-
218
- let mut fc_choices = Vec::new();
219
- for ctx in &contexts {
220
- anyhow::ensure!(!ctx.is_empty(), "context series must not be empty");
221
- eprintln!("Running forecast ({} context steps → {horizon} future steps) …", ctx.len());
222
- let quantile_mat = model.forecast(ctx, horizon).context("forecast")?;
223
-
224
- let point = quantile_mat.get(median_idx).cloned().unwrap_or_default();
225
- let mut quantiles = BTreeMap::new();
226
- for (i, &level) in quantile_levels.iter().enumerate() {
227
- if let Some(q) = quantile_mat.get(i) {
228
- quantiles.insert(format!("{level:.2}"), q.clone());
229
- }
230
- }
231
- fc_choices.push((point, quantiles));
232
- }
233
- let total_ctx: usize = contexts.iter().map(|c| c.len()).sum();
234
- println!("{}", forecast_json("flowstate-r1", total_ctx, horizon, fc_choices)?);
235
- }
236
- }
237
-
238
- Ok(())
239
- }
240
-
241
- fn parse_contexts(val: serde_json::Value) -> anyhow::Result<Vec<Vec<f32>>> {
242
- match val {
243
- serde_json::Value::Array(arr) if arr.is_empty() => {
244
- anyhow::bail!("context must be a non-empty array")
245
- }
246
- serde_json::Value::Array(arr) => {
247
- if arr.first().map(|v| v.is_array()).unwrap_or(false) {
248
- arr.into_iter()
249
- .enumerate()
250
- .map(|(i, v)| {
251
- serde_json::from_value::<Vec<f32>>(v)
252
- .with_context(|| format!("context[{i}] must be an array of numbers"))
253
- })
254
- .collect()
255
- } else {
256
- let ctx = serde_json::from_value::<Vec<f32>>(serde_json::Value::Array(arr))
257
- .context("context must be a JSON array of numbers")?;
258
- Ok(vec![ctx])
259
- }
260
- }
261
- _ => anyhow::bail!("context must be a JSON array"),
262
- }
263
- }
264
-
265
- fn forecast_json(
266
- model_name: &str,
267
- context_length: usize,
268
- forecast_length: usize,
269
- fc_choices: Vec<(Vec<f32>, BTreeMap<String, Vec<f32>>)>,
270
- ) -> anyhow::Result<String> {
271
- #[derive(Serialize)]
272
- struct ForecastResponse {
273
- id: String,
274
- object: &'static str,
275
- created: u64,
276
- model: String,
277
- choices: Vec<Choice>,
278
- usage: Usage,
279
- }
280
- #[derive(Serialize)]
281
- struct Choice {
282
- index: usize,
283
- forecast: ForecastOutput,
284
- finish_reason: &'static str,
285
- }
286
- #[derive(Serialize)]
287
- struct ForecastOutput {
288
- point: Vec<f32>,
289
- quantiles: BTreeMap<String, Vec<f32>>,
290
- }
291
- #[derive(Serialize)]
292
- struct Usage {
293
- context_length: usize,
294
- forecast_length: usize,
295
- }
296
-
297
- let created = SystemTime::now()
298
- .duration_since(UNIX_EPOCH)
299
- .unwrap_or_default()
300
- .as_secs();
301
-
302
- let resp = ForecastResponse {
303
- id: format!("forecast-{created:016x}"),
304
- object: "forecast",
305
- created,
306
- model: model_name.to_string(),
307
- choices: fc_choices.into_iter().enumerate().map(|(i, (point, quantiles))| Choice {
308
- index: i,
309
- forecast: ForecastOutput { point, quantiles },
310
- finish_reason: "stop",
311
- }).collect(),
312
- usage: Usage { context_length, forecast_length },
313
- };
314
-
315
- Ok(serde_json::to_string_pretty(&resp)?)
316
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/tensor_map.rs DELETED
@@ -1,101 +0,0 @@
1
- /// Map a FlowState HuggingFace tensor name to the GGUF canonical name.
2
- /// Returns `None` for tensors unused at inference time (caller skips them).
3
- ///
4
- /// HF prefix in safetensors: (none — model is saved as FlowStateForPrediction,
5
- /// which wraps FlowStateModel in `self.model`, but the checkpoint stores
6
- /// FlowStateModel weights directly without the `.model.` prefix.)
7
- ///
8
- /// Verified tensor names from model.safetensors header:
9
- /// embed.embed.weight / embed.embed.bias
10
- /// encoder.layers.{N}.ssm.{log_Lambda_real, Lambda_imag, B_tilde_r, B_tilde_i,
11
- /// C_tilde_r, C_tilde_i, D, log_Delta}
12
- /// encoder.layers.{N}.out.{weight, bias}
13
- /// encoder.layers.{N}.norm.{weight, bias}
14
- /// decoder.lin.{weight, bias}
15
- pub fn map_tensor_name(hf_name: &str) -> Option<String> {
16
- // Embedding
17
- match hf_name {
18
- "embed.embed.weight" => return Some("embed.weight".into()),
19
- "embed.embed.bias" => return Some("embed.bias".into()),
20
- "decoder.lin.weight" => return Some("decoder.weight".into()),
21
- "decoder.lin.bias" => return Some("decoder.bias".into()),
22
- _ => {}
23
- }
24
-
25
- // encoder.layers.{N}.ssm.* and encoder.layers.{N}.{out,norm}.*
26
- let rest = hf_name.strip_prefix("encoder.layers.")?;
27
- let dot = rest.find('.')?;
28
- let n: usize = rest[..dot].parse().ok()?;
29
- let suffix = &rest[dot + 1..];
30
-
31
- let name = match suffix {
32
- "ssm.log_Lambda_real" => format!("blk.{n}.ssm.log_lambda_real"),
33
- "ssm.Lambda_imag" => format!("blk.{n}.ssm.lambda_imag"),
34
- "ssm.B_tilde_r" => format!("blk.{n}.ssm.b_r"),
35
- "ssm.B_tilde_i" => format!("blk.{n}.ssm.b_i"),
36
- "ssm.C_tilde_r" => format!("blk.{n}.ssm.c_r"),
37
- "ssm.C_tilde_i" => format!("blk.{n}.ssm.c_i"),
38
- "ssm.D" => format!("blk.{n}.ssm.d"),
39
- "ssm.log_Delta" => format!("blk.{n}.ssm.log_delta"),
40
- "out.weight" => format!("blk.{n}.out.weight"),
41
- "out.bias" => format!("blk.{n}.out.bias"),
42
- "norm.weight" => format!("blk.{n}.norm.weight"),
43
- "norm.bias" => format!("blk.{n}.norm.bias"),
44
- _ => return None,
45
- };
46
- Some(name)
47
- }
48
-
49
- #[cfg(test)]
50
- mod tests {
51
- use super::*;
52
-
53
- #[test]
54
- fn test_embed() {
55
- assert_eq!(map_tensor_name("embed.embed.weight"), Some("embed.weight".into()));
56
- assert_eq!(map_tensor_name("embed.embed.bias"), Some("embed.bias".into()));
57
- }
58
-
59
- #[test]
60
- fn test_decoder() {
61
- assert_eq!(map_tensor_name("decoder.lin.weight"), Some("decoder.weight".into()));
62
- assert_eq!(map_tensor_name("decoder.lin.bias"), Some("decoder.bias".into()));
63
- }
64
-
65
- #[test]
66
- fn test_ssm_params() {
67
- assert_eq!(
68
- map_tensor_name("encoder.layers.0.ssm.log_Lambda_real"),
69
- Some("blk.0.ssm.log_lambda_real".into())
70
- );
71
- assert_eq!(
72
- map_tensor_name("encoder.layers.3.ssm.B_tilde_r"),
73
- Some("blk.3.ssm.b_r".into())
74
- );
75
- assert_eq!(
76
- map_tensor_name("encoder.layers.5.ssm.C_tilde_i"),
77
- Some("blk.5.ssm.c_i".into())
78
- );
79
- assert_eq!(
80
- map_tensor_name("encoder.layers.2.ssm.log_Delta"),
81
- Some("blk.2.ssm.log_delta".into())
82
- );
83
- }
84
-
85
- #[test]
86
- fn test_layer_mlp_and_norm() {
87
- assert_eq!(
88
- map_tensor_name("encoder.layers.1.out.weight"),
89
- Some("blk.1.out.weight".into())
90
- );
91
- assert_eq!(
92
- map_tensor_name("encoder.layers.4.norm.bias"),
93
- Some("blk.4.norm.bias".into())
94
- );
95
- }
96
-
97
- #[test]
98
- fn test_unknown_returns_none() {
99
- assert_eq!(map_tensor_name("something.unknown"), None);
100
- }
101
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/upload.rs DELETED
@@ -1,625 +0,0 @@
1
- use std::io::Read;
2
- use std::path::{Path, PathBuf};
3
-
4
- use anyhow::Context;
5
- use base64::Engine as _;
6
- use futures_util::StreamExt;
7
- use indicatif::{ProgressBar, ProgressStyle};
8
- use sha2::{Digest, Sha256};
9
- use tokio_util::io::ReaderStream;
10
-
11
- const HF_BASE: &str = "https://huggingface.co";
12
- const PREUPLOAD_SAMPLE: usize = 512;
13
-
14
- const SKIP_DIRS: &[&str] = &["target", ".git", "__pycache__", ".venv", "models"];
15
-
16
- // HF manages these files automatically — never delete them
17
- const KEEP_ALWAYS: &[&str] = &[".gitattributes", ".gitignore"];
18
-
19
- struct RegularFile {
20
- remote: String,
21
- content_b64: String,
22
- }
23
-
24
- struct LfsFile {
25
- local: PathBuf,
26
- remote: String,
27
- size: u64,
28
- oid: String, // sha256 hex
29
- }
30
-
31
- pub async fn run(repo_id: &str, token: &str, root: &Path) -> anyhow::Result<()> {
32
- let client = build_client(token)?;
33
-
34
- ensure_repo(&client, repo_id).await?;
35
-
36
- // List existing repo files so we can delete stale ones in the same commit
37
- let existing_files = list_repo_files(&client, repo_id).await?;
38
- if !existing_files.is_empty() {
39
- println!(" {} file(s) currently in repo", existing_files.len());
40
- }
41
-
42
- let files = gather_files(root)?;
43
- println!("Gathered {} file(s) to upload …", files.len());
44
-
45
- let (regular, lfs) = classify_files(&client, repo_id, &files).await?;
46
- println!(" {} regular, {} LFS", regular.len(), lfs.len());
47
-
48
- if !lfs.is_empty() {
49
- upload_lfs(&client, repo_id, &lfs).await?;
50
- }
51
-
52
- // Delete anything currently in the repo that we're not re-uploading
53
- let upload_paths: std::collections::HashSet<&str> = regular
54
- .iter()
55
- .map(|f| f.remote.as_str())
56
- .chain(lfs.iter().map(|f| f.remote.as_str()))
57
- .collect();
58
- let to_delete: Vec<String> = existing_files
59
- .into_iter()
60
- .filter(|p| !upload_paths.contains(p.as_str()) && !KEEP_ALWAYS.contains(&p.as_str()))
61
- .collect();
62
- if !to_delete.is_empty() {
63
- println!(" Deleting {} stale file(s)", to_delete.len());
64
- }
65
-
66
- make_commit(&client, repo_id, &regular, &lfs, &to_delete).await?;
67
-
68
- println!("Uploaded → https://huggingface.co/{repo_id}");
69
- Ok(())
70
- }
71
-
72
- fn build_client(token: &str) -> anyhow::Result<reqwest::Client> {
73
- let mut headers = reqwest::header::HeaderMap::new();
74
- headers.insert(
75
- reqwest::header::AUTHORIZATION,
76
- format!("Bearer {token}").parse().context("invalid HF token")?,
77
- );
78
- headers.insert(
79
- reqwest::header::USER_AGENT,
80
- "flowstate-r1-rs/0.1".parse().unwrap(),
81
- );
82
- Ok(reqwest::Client::builder().default_headers(headers).build()?)
83
- }
84
-
85
- async fn ensure_repo(client: &reqwest::Client, repo_id: &str) -> anyhow::Result<()> {
86
- let name = repo_id.split('/').nth(1).context("repo_id must be owner/name")?;
87
- let resp: reqwest::Response = client
88
- .post(format!("{HF_BASE}/api/repos/create"))
89
- .json(&serde_json::json!({ "name": name, "type": "model", "private": false }))
90
- .send()
91
- .await
92
- .context("create repo")?;
93
-
94
- let status = resp.status();
95
- if status.is_success() {
96
- println!("Created repo {repo_id}");
97
- } else if status.as_u16() == 409 {
98
- // already exists — fine
99
- } else {
100
- let body: String = resp.text().await.unwrap_or_default();
101
- anyhow::bail!("create repo: HTTP {status}: {body}");
102
- }
103
- Ok(())
104
- }
105
-
106
- /// List all file (blob) paths currently in the repo, following Link header pagination.
107
- async fn list_repo_files(client: &reqwest::Client, repo_id: &str) -> anyhow::Result<Vec<String>> {
108
- let mut files = Vec::new();
109
- let mut url = format!("{HF_BASE}/api/models/{repo_id}/tree/main?recursive=true&limit=1000");
110
-
111
- loop {
112
- let resp: reqwest::Response = client.get(&url).send().await.context("list repo tree")?;
113
- let status = resp.status();
114
- if status.as_u16() == 404 {
115
- return Ok(files); // repo is new / empty
116
- }
117
- if !status.is_success() {
118
- let body = resp.text().await.unwrap_or_default();
119
- anyhow::bail!("list repo tree: HTTP {status}: {body}");
120
- }
121
-
122
- // Extract next-page URL from Link header before consuming body
123
- let next_url = resp
124
- .headers()
125
- .get("link")
126
- .and_then(|v| v.to_str().ok())
127
- .and_then(parse_next_link);
128
-
129
- #[derive(serde::Deserialize)]
130
- struct TreeEntry { r#type: String, path: String }
131
- let entries: Vec<TreeEntry> = resp.json().await.context("parse tree response")?;
132
- for e in entries {
133
- if e.r#type == "file" {
134
- files.push(e.path);
135
- }
136
- }
137
-
138
- match next_url {
139
- Some(next) => url = next,
140
- None => break,
141
- }
142
- }
143
-
144
- Ok(files)
145
- }
146
-
147
- /// Parse `<url>; rel="next"` from a Link header value.
148
- fn parse_next_link(header: &str) -> Option<String> {
149
- for part in header.split(',') {
150
- let part = part.trim();
151
- if part.contains(r#"rel="next""#) {
152
- if let Some(url_part) = part.split(';').next() {
153
- let url = url_part.trim().trim_start_matches('<').trim_end_matches('>');
154
- return Some(url.to_string());
155
- }
156
- }
157
- }
158
- None
159
- }
160
-
161
- /// Ask HF which files need LFS vs regular inline upload, then prepare both lists.
162
- async fn classify_files(
163
- client: &reqwest::Client,
164
- repo_id: &str,
165
- files: &[(PathBuf, String)],
166
- ) -> anyhow::Result<(Vec<RegularFile>, Vec<LfsFile>)> {
167
- let mut preupload_entries: Vec<serde_json::Value> = Vec::new();
168
- for (local, remote) in files {
169
- let size = std::fs::metadata(local)
170
- .with_context(|| format!("stat {}", local.display()))?
171
- .len();
172
- let sample = {
173
- let bytes = std::fs::read(local)
174
- .with_context(|| format!("read {}", local.display()))?;
175
- let n = bytes.len().min(PREUPLOAD_SAMPLE);
176
- base64::engine::general_purpose::STANDARD.encode(&bytes[..n])
177
- };
178
- preupload_entries.push(serde_json::json!({
179
- "path": remote,
180
- "size": size,
181
- "sample": sample,
182
- }));
183
- }
184
-
185
- let url = format!("{HF_BASE}/api/models/{repo_id}/preupload/main");
186
- let resp: reqwest::Response = client
187
- .post(&url)
188
- .json(&serde_json::json!({ "files": preupload_entries }))
189
- .send()
190
- .await
191
- .context("preupload request")?;
192
-
193
- let status = resp.status();
194
- if !status.is_success() {
195
- let body: String = resp.text().await.unwrap_or_default();
196
- anyhow::bail!("preupload: HTTP {status}: {body}");
197
- }
198
-
199
- #[derive(serde::Deserialize)]
200
- struct PreuploadFile {
201
- path: String,
202
- #[serde(rename = "uploadMode")]
203
- upload_mode: String,
204
- #[serde(rename = "shouldIgnore", default)]
205
- _should_ignore: bool,
206
- }
207
- #[derive(serde::Deserialize)]
208
- struct PreuploadResp { files: Vec<PreuploadFile> }
209
-
210
- let preupload: PreuploadResp = resp.json().await.context("parse preupload response")?;
211
- let modes: std::collections::HashMap<String, String> = preupload.files
212
- .into_iter()
213
- .map(|f| (f.path, f.upload_mode))
214
- .collect();
215
-
216
- let mut regular: Vec<RegularFile> = Vec::new();
217
- let mut lfs: Vec<LfsFile> = Vec::new();
218
-
219
- for (local, remote) in files {
220
- let mode = modes.get(remote).map(|s| s.as_str()).unwrap_or("regular");
221
- if mode == "lfs" {
222
- let size = std::fs::metadata(local)?.len();
223
- let oid = sha256_file(local)?;
224
- lfs.push(LfsFile { local: local.clone(), remote: remote.clone(), size, oid });
225
- } else {
226
- let bytes = std::fs::read(local)
227
- .with_context(|| format!("read {}", local.display()))?;
228
- let content_b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
229
- regular.push(RegularFile { remote: remote.clone(), content_b64 });
230
- }
231
- }
232
-
233
- Ok((regular, lfs))
234
- }
235
-
236
- fn gather_files(root: &Path) -> anyhow::Result<Vec<(PathBuf, String)>> {
237
- let mut out = Vec::new();
238
- walk_dir(root, "", &mut out)?;
239
-
240
- // Promote the README.md from the first-level crate subdirectory to the repo root
241
- // so it renders on the HF repo page (e.g. "toto-rs/README.md" → "README.md").
242
- let has_root_readme = out.iter().any(|(_, r)| r == "README.md");
243
- if !has_root_readme {
244
- for (_, remote) in &mut out {
245
- let parts: Vec<&str> = remote.splitn(3, '/').collect();
246
- if parts.len() == 2 && parts[1] == "README.md" {
247
- *remote = "README.md".to_string();
248
- break;
249
- }
250
- }
251
- }
252
-
253
- Ok(out)
254
- }
255
-
256
- fn walk_dir(dir: &Path, prefix: &str, out: &mut Vec<(PathBuf, String)>) -> anyhow::Result<()> {
257
- let mut entries: Vec<_> = std::fs::read_dir(dir)
258
- .with_context(|| format!("read_dir {}", dir.display()))?
259
- .collect::<Result<_, _>>()?;
260
- entries.sort_by_key(|e| e.file_name());
261
- for entry in entries {
262
- let path = entry.path();
263
- let name = entry
264
- .file_name()
265
- .into_string()
266
- .map_err(|_| anyhow::anyhow!("non-UTF-8 filename"))?;
267
- if SKIP_DIRS.contains(&name.as_str()) {
268
- continue;
269
- }
270
- let remote = if prefix.is_empty() { name.clone() } else { format!("{prefix}/{name}") };
271
- if path.is_file() {
272
- out.push((path, remote));
273
- } else if path.is_dir() {
274
- walk_dir(&path, &remote, out)?;
275
- }
276
- }
277
- Ok(())
278
- }
279
-
280
- fn sha256_file(path: &Path) -> anyhow::Result<String> {
281
- let mut f =
282
- std::fs::File::open(path).with_context(|| format!("open {}", path.display()))?;
283
- let mut hasher = Sha256::new();
284
- let mut buf = vec![0u8; 8 * 1024 * 1024];
285
- loop {
286
- let n = f.read(&mut buf)?;
287
- if n == 0 { break; }
288
- hasher.update(&buf[..n]);
289
- }
290
- Ok(format!("{:x}", hasher.finalize()))
291
- }
292
-
293
- async fn upload_lfs(
294
- client: &reqwest::Client,
295
- repo_id: &str,
296
- files: &[LfsFile],
297
- ) -> anyhow::Result<()> {
298
- let objects: Vec<_> = files
299
- .iter()
300
- .map(|f| serde_json::json!({ "oid": f.oid, "size": f.size }))
301
- .collect();
302
-
303
- // Request both multipart (for files >5 GB) and basic transfers
304
- let url = format!("{HF_BASE}/{repo_id}.git/info/lfs/objects/batch");
305
- let lfs_body = serde_json::to_string(&serde_json::json!({
306
- "operation": "upload",
307
- "transfers": ["multipart", "basic"],
308
- "objects": objects,
309
- }))?;
310
- let resp: reqwest::Response = client
311
- .post(&url)
312
- .header("Content-Type", "application/vnd.git-lfs+json")
313
- .header("Accept", "application/vnd.git-lfs+json")
314
- .body(lfs_body)
315
- .send()
316
- .await
317
- .context("LFS batch request")?;
318
-
319
- let status = resp.status();
320
- if !status.is_success() {
321
- let body: String = resp.text().await.unwrap_or_default();
322
- anyhow::bail!("LFS batch: HTTP {status}: {body}");
323
- }
324
-
325
- #[derive(serde::Deserialize)]
326
- struct BatchResp { objects: Vec<serde_json::Value> }
327
- let batch: BatchResp = resp.json().await.context("parse LFS batch response")?;
328
-
329
- for (file, obj) in files.iter().zip(batch.objects.iter()) {
330
- let Some(upload_href) =
331
- obj.pointer("/actions/upload/href").and_then(|v: &serde_json::Value| v.as_str())
332
- else {
333
- println!(" (already on LFS) {}", file.remote);
334
- continue;
335
- };
336
-
337
- // Multipart if the server provided chunk_size in the header
338
- let is_multipart = obj.pointer("/actions/upload/header/chunk_size").is_some();
339
-
340
- if is_multipart {
341
- upload_lfs_object_multipart(file, upload_href, obj).await
342
- .with_context(|| format!("upload (multipart) {}", file.remote))?;
343
- } else {
344
- upload_lfs_object(file, upload_href, obj).await
345
- .with_context(|| format!("upload {}", file.remote))?;
346
- }
347
-
348
- // Verify step (optional but recommended by Git LFS spec)
349
- if let Some(verify_href) =
350
- obj.pointer("/actions/verify/href").and_then(|v: &serde_json::Value| v.as_str())
351
- {
352
- let verify_headers = obj
353
- .pointer("/actions/verify/header")
354
- .and_then(|v: &serde_json::Value| v.as_object())
355
- .cloned()
356
- .unwrap_or_default();
357
-
358
- let mut vreq: reqwest::RequestBuilder = reqwest::Client::new()
359
- .post(verify_href)
360
- .header("Content-Type", "application/vnd.git-lfs+json")
361
- .json(&serde_json::json!({ "oid": file.oid, "size": file.size }));
362
- for (k, v) in &verify_headers {
363
- if let Some(val) = v.as_str() {
364
- vreq = vreq.header(k.as_str(), val);
365
- }
366
- }
367
- let vresp: reqwest::Response = vreq.send().await.context("LFS verify")?;
368
- if !vresp.status().is_success() {
369
- eprintln!(" Warning: LFS verify returned {}", vresp.status());
370
- }
371
- }
372
- }
373
-
374
- Ok(())
375
- }
376
-
377
- async fn upload_lfs_object(
378
- file: &LfsFile,
379
- href: &str,
380
- obj: &serde_json::Value,
381
- ) -> anyhow::Result<()> {
382
- let pb = ProgressBar::new(file.size);
383
- pb.set_style(
384
- ProgressStyle::with_template(
385
- " {msg} [{bar:40}] {bytes}/{total_bytes} ({bytes_per_sec}, eta {eta})",
386
- )
387
- .unwrap()
388
- .progress_chars("=>-"),
389
- );
390
- pb.set_message(file.remote.clone());
391
-
392
- let f = tokio::fs::File::open(&file.local)
393
- .await
394
- .with_context(|| format!("open {}", file.local.display()))?;
395
-
396
- let pb2 = pb.clone();
397
- let stream = ReaderStream::new(f).map(move |chunk| {
398
- if let Ok(ref b) = chunk {
399
- pb2.inc(b.len() as u64);
400
- }
401
- chunk
402
- });
403
-
404
- // LFS upload goes to S3 / Azure — use a plain client (no HF auth header)
405
- let mut req = reqwest::Client::new()
406
- .put(href)
407
- .header("Content-Length", file.size.to_string());
408
-
409
- if let Some(extra) = obj.pointer("/actions/upload/header").and_then(|v| v.as_object()) {
410
- for (k, v) in extra {
411
- if let Some(val) = v.as_str() {
412
- req = req.header(k.as_str(), val);
413
- }
414
- }
415
- }
416
-
417
- let resp = req
418
- .body(reqwest::Body::wrap_stream(stream))
419
- .send()
420
- .await
421
- .context("PUT LFS object")?;
422
-
423
- pb.finish_and_clear();
424
-
425
- let status = resp.status();
426
- if !status.is_success() {
427
- let body = resp.text().await.unwrap_or_default();
428
- anyhow::bail!("LFS PUT HTTP {status}: {body}");
429
- }
430
-
431
- Ok(())
432
- }
433
-
434
- async fn upload_lfs_object_multipart(
435
- file: &LfsFile,
436
- complete_href: &str,
437
- obj: &serde_json::Value,
438
- ) -> anyhow::Result<()> {
439
- use tokio::io::AsyncReadExt;
440
-
441
- let header = obj
442
- .pointer("/actions/upload/header")
443
- .and_then(|v| v.as_object())
444
- .ok_or_else(|| anyhow::anyhow!("no header in multipart LFS response"))?;
445
-
446
- let chunk_size: usize = header
447
- .get("chunk_size")
448
- .and_then(|v| v.as_str())
449
- .and_then(|s| s.parse().ok())
450
- .ok_or_else(|| anyhow::anyhow!("missing chunk_size in multipart header"))?;
451
-
452
- // Collect part URLs sorted numerically by key ("00001", "00002", …)
453
- let mut parts: Vec<(u32, String)> = header
454
- .iter()
455
- .filter_map(|(k, v)| {
456
- let n: u32 = k.parse().ok()?;
457
- Some((n, v.as_str()?.to_string()))
458
- })
459
- .collect();
460
- parts.sort_by_key(|(n, _)| *n);
461
-
462
- let pb = ProgressBar::new(file.size);
463
- pb.set_style(
464
- ProgressStyle::with_template(
465
- " {msg} [{bar:40}] {bytes}/{total_bytes} ({bytes_per_sec}, eta {eta})",
466
- )
467
- .unwrap()
468
- .progress_chars("=>-"),
469
- );
470
- pb.set_message(file.remote.clone());
471
-
472
- let mut f = tokio::fs::File::open(&file.local)
473
- .await
474
- .with_context(|| format!("open {}", file.local.display()))?;
475
-
476
- let s3 = reqwest::Client::new(); // plain client — S3 parts use pre-signed URLs
477
- let mut etags: Vec<(u32, String)> = Vec::with_capacity(parts.len());
478
-
479
- for (part_num, url) in &parts {
480
- // Read up to chunk_size bytes for this part
481
- let mut buf = vec![0u8; chunk_size];
482
- let mut pos = 0;
483
- while pos < chunk_size {
484
- let n = f.read(&mut buf[pos..]).await?;
485
- if n == 0 { break; }
486
- pos += n;
487
- }
488
- if pos == 0 { break; }
489
- buf.truncate(pos);
490
- let len = buf.len();
491
-
492
- // Retry up to 3 times on transient connection errors
493
- const MAX_RETRIES: usize = 3;
494
- let mut last_err: Option<anyhow::Error> = None;
495
- let mut etag_opt: Option<String> = None;
496
- for attempt in 0..MAX_RETRIES {
497
- if attempt > 0 {
498
- tokio::time::sleep(std::time::Duration::from_secs(2)).await;
499
- eprintln!(" Retrying part {part_num} (attempt {})…", attempt + 1);
500
- }
501
- match s3
502
- .put(url.as_str())
503
- .header("Content-Length", len.to_string())
504
- .body(buf.clone())
505
- .send()
506
- .await
507
- {
508
- Err(e) => {
509
- last_err = Some(anyhow::anyhow!("PUT part {part_num}: {e}"));
510
- }
511
- Ok(resp) => {
512
- let status = resp.status();
513
- let tag = resp.headers().get("etag")
514
- .and_then(|v| v.to_str().ok())
515
- .map(|s| s.to_string());
516
- if status.is_success() {
517
- if let Some(t) = tag {
518
- etag_opt = Some(t);
519
- last_err = None;
520
- break;
521
- } else {
522
- last_err = Some(anyhow::anyhow!("PUT part {part_num}: no ETag in response"));
523
- }
524
- } else {
525
- let body = resp.text().await.unwrap_or_default();
526
- last_err = Some(anyhow::anyhow!("PUT part {part_num}: HTTP {status}: {body}"));
527
- }
528
- }
529
- }
530
- }
531
- let etag = etag_opt.ok_or_else(|| {
532
- last_err.unwrap_or_else(|| anyhow::anyhow!("PUT part {part_num}: exhausted retries"))
533
- })?;
534
-
535
- pb.inc(len as u64);
536
- etags.push((*part_num, etag));
537
- }
538
-
539
- pb.finish_and_clear();
540
-
541
- // Tell HF to assemble the parts on S3
542
- let parts_json: Vec<serde_json::Value> = etags
543
- .iter()
544
- .map(|(n, e)| serde_json::json!({ "partNumber": n, "etag": e }))
545
- .collect();
546
-
547
- let resp = s3
548
- .post(complete_href)
549
- .json(&serde_json::json!({ "oid": file.oid, "parts": parts_json }))
550
- .send()
551
- .await
552
- .context("complete multipart")?;
553
-
554
- let status = resp.status();
555
- if !status.is_success() {
556
- let body = resp.text().await.unwrap_or_default();
557
- anyhow::bail!("complete multipart: HTTP {status}: {body}");
558
- }
559
-
560
- Ok(())
561
- }
562
-
563
- async fn make_commit(
564
- client: &reqwest::Client,
565
- repo_id: &str,
566
- regular: &[RegularFile],
567
- lfs: &[LfsFile],
568
- to_delete: &[String],
569
- ) -> anyhow::Result<()> {
570
- let mut lines: Vec<String> = Vec::new();
571
-
572
- lines.push(serde_json::to_string(&serde_json::json!({
573
- "key": "header",
574
- "value": { "summary": "Upload model files", "description": "" },
575
- }))?);
576
-
577
- for rf in regular {
578
- lines.push(serde_json::to_string(&serde_json::json!({
579
- "key": "file",
580
- "value": {
581
- "path": rf.remote,
582
- "encoding": "base64",
583
- "content": rf.content_b64,
584
- },
585
- }))?);
586
- }
587
-
588
- for lf in lfs {
589
- lines.push(serde_json::to_string(&serde_json::json!({
590
- "key": "lfsFile",
591
- "value": {
592
- "path": lf.remote,
593
- "algo": "sha256",
594
- "oid": lf.oid,
595
- "size": lf.size,
596
- },
597
- }))?);
598
- }
599
-
600
- for path in to_delete {
601
- lines.push(serde_json::to_string(&serde_json::json!({
602
- "key": "deletedEntry",
603
- "value": { "path": path },
604
- }))?);
605
- }
606
-
607
- let body = lines.join("\n");
608
-
609
- let url = format!("{HF_BASE}/api/models/{repo_id}/commit/main");
610
- let resp = client
611
- .post(&url)
612
- .header("Content-Type", "application/x-ndjson")
613
- .body(body)
614
- .send()
615
- .await
616
- .context("POST commit")?;
617
-
618
- let status = resp.status();
619
- if !status.is_success() {
620
- let body = resp.text().await.unwrap_or_default();
621
- anyhow::bail!("commit: HTTP {status}: {body}");
622
- }
623
-
624
- Ok(())
625
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Free AI Image Generator No sign-up. Instant results. Open Now