amaye15 commited on
Commit
bf12da9
·
verified ·
1 Parent(s): 1f44340

Upload model files

Browse files
README.md CHANGED
@@ -1,19 +1,29 @@
1
  ---
2
  license: mit
3
  library_name: gguf
 
 
 
 
 
 
4
  tags:
5
  - gguf
6
  - time-series
7
  - forecasting
 
 
 
 
8
  - rust
9
- base_model: google/timesfm-2.5-200m-pytorch
10
  ---
11
 
12
  # timesfm-rs
13
 
14
  Pure Rust converter and inference engine for [google/timesfm-2.5-200m-pytorch](https://huggingface.co/google/timesfm-2.5-200m-pytorch).
15
 
16
- Produces GGUF v3 files and runs native forecasting — no Python required.
17
 
18
  ## Build
19
 
@@ -78,9 +88,9 @@ Output is JSON in an OpenAI-compatible forecast format with point forecast and a
78
  "forecast": {
79
  "point": [2.1, 2.3, 2.5, "..."],
80
  "quantiles": {
81
- "0.1": [1.8, 2.0, 2.2, "..."],
82
- "0.5": [2.1, 2.3, 2.5, "..."],
83
- "0.9": [2.4, 2.6, 2.8, "..."]
84
  }
85
  },
86
  "finish_reason": "stop"
 
1
  ---
2
  license: mit
3
  library_name: gguf
4
+ pipeline_tag: time-series-forecasting
5
+ language:
6
+ - en
7
+ base_model: google/timesfm-2.5-200m-pytorch
8
+ base_model_relation: quantized
9
+ quantized_by: amaye15
10
  tags:
11
  - gguf
12
  - time-series
13
  - forecasting
14
+ - zero-shot
15
+ - probabilistic
16
+ - transformer
17
+ - patch-based
18
  - rust
19
+ inference: false
20
  ---
21
 
22
  # timesfm-rs
23
 
24
  Pure Rust converter and inference engine for [google/timesfm-2.5-200m-pytorch](https://huggingface.co/google/timesfm-2.5-200m-pytorch).
25
 
26
+ Pre-converted GGUF files are available at [amaye15/timesfm-gguf](https://huggingface.co/amaye15/timesfm-gguf). Produces GGUF v3 files and runs native forecasting — no Python required.
27
 
28
  ## Build
29
 
 
88
  "forecast": {
89
  "point": [2.1, 2.3, 2.5, "..."],
90
  "quantiles": {
91
+ "0.10": [1.8, 2.0, 2.2, "..."],
92
+ "0.50": [2.1, 2.3, 2.5, "..."],
93
+ "0.90": [2.4, 2.6, 2.8, "..."]
94
  }
95
  },
96
  "finish_reason": "stop"
timesfm-rs/.cargo/config.toml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [build]
2
+ rustflags = ["-C", "target-cpu=native"]
timesfm-rs/Cargo.toml CHANGED
@@ -26,7 +26,11 @@ 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]
 
26
  candle-nn = { version = "0.8", features = ["accelerate"] }
27
 
28
  [profile.release]
29
+ opt-level = 3
30
+ lto = "fat"
31
+ codegen-units = 1
32
+ strip = true
33
+ panic = "abort"
34
 
35
 
36
  [features]
timesfm-rs/scripts/compare_python.py CHANGED
@@ -18,8 +18,8 @@ TIMESFM_SRC = os.path.join(REPO_ROOT, "..", "timesfm", "src")
18
  if os.path.isdir(TIMESFM_SRC):
19
  sys.path.insert(0, TIMESFM_SRC)
20
 
21
- QUANTILE_LABELS = ["point", "q0.1", "q0.2", "q0.3", "q0.4", "q0.5", "q0.6", "q0.7", "q0.8", "q0.9"]
22
- QUANTILE_KEYS = ["0.1", "0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9"]
23
 
24
 
25
  def make_context(n=128):
 
18
  if os.path.isdir(TIMESFM_SRC):
19
  sys.path.insert(0, TIMESFM_SRC)
20
 
21
+ QUANTILE_LABELS = ["point", "q0.10", "q0.20", "q0.30", "q0.40", "q0.50", "q0.60", "q0.70", "q0.80", "q0.90"]
22
+ QUANTILE_KEYS = ["0.10", "0.20", "0.30", "0.40", "0.50", "0.60", "0.70", "0.80", "0.90"]
23
 
24
 
25
  def make_context(n=128):
timesfm-rs/src/infer/mod.rs CHANGED
@@ -201,7 +201,7 @@ impl TimesFMModel {
201
  &content, &mut reader, "out_point", D_MODEL, D_MODEL, false, &device,
202
  )?;
203
 
204
- let rope = RopeCache::new(HEAD_DIM, MAX_SEQ, ROPE_THETA);
205
 
206
  Ok(Self { device, rope, tokenizer, blocks, out_point, causal_mask_cache: Mutex::new(HashMap::new()) })
207
  }
@@ -395,12 +395,12 @@ impl TimesFMModel {
395
  let cached_len = rope_offset;
396
  let decode_mask = make_decode_mask(M_PATCHES, cached_len, &self.device)?;
397
  for (li, block) in self.blocks.iter().enumerate() {
 
 
398
  let (h_out, k_new, v_new) =
399
  self.decode_block_kv(hidden, block, &kv_cache[li], rope_offset, &decode_mask)?;
400
  hidden = h_out;
401
- let k_full = Tensor::cat(&[&kv_cache[li].0, &k_new], 2)?;
402
- let v_full = Tensor::cat(&[&kv_cache[li].1, &v_new], 2)?;
403
- kv_cache[li] = (k_full, v_full);
404
  }
405
  let out_seq = hidden.squeeze(0)?;
406
  forward_residual_block(&out_seq, &self.out_point, false)
@@ -452,7 +452,9 @@ impl TimesFMModel {
452
  let k = k.permute([0, 2, 1, 3])?.contiguous()?;
453
  let v = v.permute([0, 2, 1, 3])?.contiguous()?;
454
 
455
- let k_full = Tensor::cat(&[&cache.0, &k], 2)?; // [1, N_HEADS, cached+M, HEAD_DIM]
 
 
456
  let v_full = Tensor::cat(&[&cache.1, &v], 2)?;
457
 
458
  let scores = q.matmul(&k_full.transpose(D::Minus1, D::Minus2)?)?;
@@ -460,7 +462,7 @@ impl TimesFMModel {
460
  let attn_w = candle_nn::ops::softmax(&scores, D::Minus1)?;
461
  let ctx = attn_w.matmul(&v_full)?;
462
  let ctx = ctx.permute([0, 2, 1, 3])?.contiguous()?.reshape((1, M_PATCHES, D_MODEL))?;
463
- Ok((linear(&ctx, &w.out_w, None)?, k, v))
464
  }
465
 
466
  fn forward_ffn(&self, x: Tensor, w: &FfnW) -> Result<Tensor> {
 
201
  &content, &mut reader, "out_point", D_MODEL, D_MODEL, false, &device,
202
  )?;
203
 
204
+ let rope = RopeCache::new(HEAD_DIM, MAX_SEQ, ROPE_THETA, &device)?;
205
 
206
  Ok(Self { device, rope, tokenizer, blocks, out_point, causal_mask_cache: Mutex::new(HashMap::new()) })
207
  }
 
395
  let cached_len = rope_offset;
396
  let decode_mask = make_decode_mask(M_PATCHES, cached_len, &self.device)?;
397
  for (li, block) in self.blocks.iter().enumerate() {
398
+ // decode_block_kv returns the extended K/V (old cache + new M_PATCHES).
399
+ // Store directly — no second Tensor::cat needed.
400
  let (h_out, k_new, v_new) =
401
  self.decode_block_kv(hidden, block, &kv_cache[li], rope_offset, &decode_mask)?;
402
  hidden = h_out;
403
+ kv_cache[li] = (k_new, v_new);
 
 
404
  }
405
  let out_seq = hidden.squeeze(0)?;
406
  forward_residual_block(&out_seq, &self.out_point, false)
 
452
  let k = k.permute([0, 2, 1, 3])?.contiguous()?;
453
  let v = v.permute([0, 2, 1, 3])?.contiguous()?;
454
 
455
+ // Extend cache: [1, N_HEADS, cached+M, HEAD_DIM].
456
+ // Return k_full/v_full so the caller can store them directly without a second cat.
457
+ let k_full = Tensor::cat(&[&cache.0, &k], 2)?;
458
  let v_full = Tensor::cat(&[&cache.1, &v], 2)?;
459
 
460
  let scores = q.matmul(&k_full.transpose(D::Minus1, D::Minus2)?)?;
 
462
  let attn_w = candle_nn::ops::softmax(&scores, D::Minus1)?;
463
  let ctx = attn_w.matmul(&v_full)?;
464
  let ctx = ctx.permute([0, 2, 1, 3])?.contiguous()?.reshape((1, M_PATCHES, D_MODEL))?;
465
+ Ok((linear(&ctx, &w.out_w, None)?, k_full, v_full))
466
  }
467
 
468
  fn forward_ffn(&self, x: Tensor, w: &FfnW) -> Result<Tensor> {
timesfm-rs/src/infer/rope.rs CHANGED
@@ -1,14 +1,13 @@
1
  use anyhow::Result;
2
- use candle_core::{DType, Tensor, D};
3
 
4
  pub struct RopeCache {
5
- cos: Vec<f32>,
6
- sin: Vec<f32>,
7
- head_dim: usize,
8
  }
9
 
10
  impl RopeCache {
11
- pub fn new(head_dim: usize, max_seq: usize, theta: f64) -> Self {
12
  let half = head_dim / 2;
13
  let inv_freq: Vec<f64> = (0..half)
14
  .map(|i| 1.0 / theta.powf(2.0 * i as f64 / head_dim as f64))
@@ -26,7 +25,9 @@ impl RopeCache {
26
  sin[p * head_dim + half + i] = s;
27
  }
28
  }
29
- Self { cos, sin, head_dim }
 
 
30
  }
31
 
32
  /// Apply RoPE to x with shape [b, seq, n_heads, head_dim].
@@ -38,16 +39,10 @@ impl RopeCache {
38
  let hd = dims[3];
39
  let half = hd / 2;
40
  let dtype = x.dtype();
41
- let device = x.device();
42
 
43
- let cos_slice: Vec<f32> = self.cos[start_pos * hd..(start_pos + seq) * hd].to_vec();
44
- let sin_slice: Vec<f32> = self.sin[start_pos * hd..(start_pos + seq) * hd].to_vec();
45
-
46
- // [seq, hd] → [1, seq, 1, hd] for broadcasting over [b, seq, nh, hd]
47
- let cos_t = Tensor::from_vec(cos_slice, (seq, hd), device)?
48
- .unsqueeze(0)?.unsqueeze(2)?;
49
- let sin_t = Tensor::from_vec(sin_slice, (seq, hd), device)?
50
- .unsqueeze(0)?.unsqueeze(2)?;
51
 
52
  let x32 = x.to_dtype(DType::F32)?;
53
  let x1 = x32.narrow(D::Minus1, 0, half)?;
@@ -55,7 +50,7 @@ impl RopeCache {
55
  let cos1 = cos_t.narrow(D::Minus1, 0, half)?;
56
  let sin1 = sin_t.narrow(D::Minus1, 0, half)?;
57
 
58
- let first = (x1.broadcast_mul(&cos1)? - x2.broadcast_mul(&sin1)?)?;
59
  let second = (x2.broadcast_mul(&cos1)? + x1.broadcast_mul(&sin1)?)?;
60
 
61
  Ok(Tensor::cat(&[&first, &second], D::Minus1)?.to_dtype(dtype)?)
 
1
  use anyhow::Result;
2
+ use candle_core::{DType, Device, Tensor, D};
3
 
4
  pub struct RopeCache {
5
+ cos_t: Tensor, // [max_seq, head_dim]
6
+ sin_t: Tensor, // [max_seq, head_dim]
 
7
  }
8
 
9
  impl RopeCache {
10
+ pub fn new(head_dim: usize, max_seq: usize, theta: f64, device: &Device) -> Result<Self> {
11
  let half = head_dim / 2;
12
  let inv_freq: Vec<f64> = (0..half)
13
  .map(|i| 1.0 / theta.powf(2.0 * i as f64 / head_dim as f64))
 
25
  sin[p * head_dim + half + i] = s;
26
  }
27
  }
28
+ let cos_t = Tensor::from_vec(cos, (max_seq, head_dim), device)?;
29
+ let sin_t = Tensor::from_vec(sin, (max_seq, head_dim), device)?;
30
+ Ok(Self { cos_t, sin_t })
31
  }
32
 
33
  /// Apply RoPE to x with shape [b, seq, n_heads, head_dim].
 
39
  let hd = dims[3];
40
  let half = hd / 2;
41
  let dtype = x.dtype();
 
42
 
43
+ // Zero-copy narrow: produces a view into the pre-built Tensor
44
+ let cos_t = self.cos_t.narrow(0, start_pos, seq)?.unsqueeze(0)?.unsqueeze(2)?;
45
+ let sin_t = self.sin_t.narrow(0, start_pos, seq)?.unsqueeze(0)?.unsqueeze(2)?;
 
 
 
 
 
46
 
47
  let x32 = x.to_dtype(DType::F32)?;
48
  let x1 = x32.narrow(D::Minus1, 0, half)?;
 
50
  let cos1 = cos_t.narrow(D::Minus1, 0, half)?;
51
  let sin1 = sin_t.narrow(D::Minus1, 0, half)?;
52
 
53
+ let first = (x1.broadcast_mul(&cos1)? - x2.broadcast_mul(&sin1)?)?;
54
  let second = (x2.broadcast_mul(&cos1)? + x1.broadcast_mul(&sin1)?)?;
55
 
56
  Ok(Tensor::cat(&[&first, &second], D::Minus1)?.to_dtype(dtype)?)
timesfm-rs/src/main.rs CHANGED
@@ -155,7 +155,7 @@ fn cmd_infer(gguf_path: &PathBuf) -> Result<()> {
155
  eprintln!("Loading model from {} …", gguf_path.display());
156
  let model = TimesFMModel::load(gguf_path)?;
157
 
158
- let quantile_labels = ["0.1", "0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9"];
159
 
160
  let mut fc_choices = Vec::new();
161
  for ctx in &contexts {
 
155
  eprintln!("Loading model from {} …", gguf_path.display());
156
  let model = TimesFMModel::load(gguf_path)?;
157
 
158
+ let quantile_labels = ["0.10", "0.20", "0.30", "0.40", "0.50", "0.60", "0.70", "0.80", "0.90"];
159
 
160
  let mut fc_choices = Vec::new();
161
  for ctx in &contexts {
timesfm-rs/src/py.rs CHANGED
@@ -69,7 +69,7 @@ impl TimesFM {
69
  horizon: usize,
70
  ) -> PyResult<Py<PyAny>> {
71
  let contexts = parse_contexts(context)?;
72
- let quantile_labels = ["0.1", "0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9"];
73
  let mut fc_choices = Vec::new();
74
  for ctx in &contexts {
75
  if ctx.is_empty() {
 
69
  horizon: usize,
70
  ) -> PyResult<Py<PyAny>> {
71
  let contexts = parse_contexts(context)?;
72
+ let quantile_labels = ["0.10", "0.20", "0.30", "0.40", "0.50", "0.60", "0.70", "0.80", "0.90"];
73
  let mut fc_choices = Vec::new();
74
  for ctx in &contexts {
75
  if ctx.is_empty() {