Upload model files
Browse files- README.md +15 -5
- timesfm-rs/.cargo/config.toml +2 -0
- timesfm-rs/Cargo.toml +5 -1
- timesfm-rs/scripts/compare_python.py +2 -2
- timesfm-rs/src/infer/mod.rs +8 -6
- timesfm-rs/src/infer/rope.rs +11 -16
- timesfm-rs/src/main.rs +1 -1
- timesfm-rs/src/py.rs +1 -1
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 |
-
|
| 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.
|
| 82 |
-
"0.
|
| 83 |
-
"0.
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 22 |
-
QUANTILE_KEYS = ["0.
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 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)?,
|
| 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 |
-
|
| 6 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 44 |
-
let
|
| 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
|
| 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.
|
| 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.
|
| 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() {
|