Time Series Forecasting
GGUF
Rust
English
time-series
forecasting
zero-shot
probabilistic
state-space-model
Instructions to use amaye15/flowstate-r1-gguf with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use amaye15/flowstate-r1-gguf with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf amaye15/flowstate-r1-gguf:F16 # Run inference directly in the terminal: llama cli -hf amaye15/flowstate-r1-gguf:F16
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf amaye15/flowstate-r1-gguf:F16 # Run inference directly in the terminal: llama cli -hf amaye15/flowstate-r1-gguf:F16
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf amaye15/flowstate-r1-gguf:F16 # Run inference directly in the terminal: ./llama-cli -hf amaye15/flowstate-r1-gguf:F16
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf amaye15/flowstate-r1-gguf:F16 # Run inference directly in the terminal: ./build/bin/llama-cli -hf amaye15/flowstate-r1-gguf:F16
Use Docker
docker model run hf.co/amaye15/flowstate-r1-gguf:F16
- LM Studio
- Jan
- Ollama
How to use amaye15/flowstate-r1-gguf with Ollama:
ollama run hf.co/amaye15/flowstate-r1-gguf:F16
- Unsloth Studio
How to use amaye15/flowstate-r1-gguf with Unsloth Studio:
Install Unsloth Studio (macOS, Linux, WSL)
curl -fsSL https://unsloth.ai/install.sh | sh # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for amaye15/flowstate-r1-gguf to start chatting
Install Unsloth Studio (Windows)
irm https://unsloth.ai/install.ps1 | iex # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for amaye15/flowstate-r1-gguf to start chatting
Using HuggingFace Spaces for Unsloth
# No setup required # Open https://huggingface.co/spaces/unsloth/studio in your browser # Search for amaye15/flowstate-r1-gguf to start chatting
- Docker Model Runner
How to use amaye15/flowstate-r1-gguf with Docker Model Runner:
docker model run hf.co/amaye15/flowstate-r1-gguf:F16
- Lemonade
How to use amaye15/flowstate-r1-gguf with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull amaye15/flowstate-r1-gguf:F16
Run and chat with the model
lemonade run user.flowstate-r1-gguf-F16
List all available models
lemonade list
- Atomic Chat
Upload model files
Browse files- Cargo.toml +0 -37
- LICENSE +0 -21
- flowstate-r1-rs/Cargo.lock +64 -0
- flowstate-r1-rs/Cargo.toml +9 -0
- flowstate-r1-rs/pyproject.toml +17 -0
- flowstate-r1-rs/src/infer/mod.rs +146 -195
- {src → flowstate-r1-rs/src}/lib.rs +0 -0
- flowstate-r1-rs/src/main.rs +48 -27
- {src → flowstate-r1-rs/src}/py.rs +0 -0
- flowstate-r1-rs/uv.lock +96 -0
- pyproject.toml +9 -9
- scripts/compare_python.py +0 -138
- scripts/convert_all.sh +0 -30
- scripts/upload.sh +0 -20
- src/config.rs +0 -51
- src/convert.rs +0 -265
- src/download.rs +0 -182
- src/gguf/mod.rs +0 -5
- src/gguf/types.rs +0 -86
- src/gguf/writer.rs +0 -161
- src/infer/mod.rs +0 -557
- src/main.rs +0 -316
- src/tensor_map.rs +0 -101
- src/upload.rs +0 -625
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]
|
| 33 |
b_i: Tensor, // [state_dim, embed_dim]
|
| 34 |
-
c_r: Tensor, // [embed_dim, state_dim]
|
| 35 |
c_i: Tensor, // [embed_dim, state_dim]
|
| 36 |
-
d:
|
| 37 |
log_delta: Vec<f32>, // [state_dim]
|
| 38 |
}
|
| 39 |
|
| 40 |
struct BlockWeights {
|
| 41 |
ssm: S5Weights,
|
| 42 |
-
out_weight: Tensor,
|
| 43 |
-
out_bias:
|
| 44 |
-
norm_weight:
|
| 45 |
-
norm_bias:
|
|
|
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
pub struct FlowStateModel {
|
| 49 |
device: Device,
|
| 50 |
config: InferConfig,
|
| 51 |
-
embed_w: Tensor,
|
| 52 |
-
embed_b:
|
| 53 |
blocks: Vec<BlockWeights>,
|
| 54 |
-
decoder_w: Tensor,
|
| 55 |
-
decoder_b:
|
|
|
|
| 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 =
|
| 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:
|
| 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"),
|
| 114 |
-
out_bias:
|
| 115 |
-
norm_weight:
|
| 116 |
-
norm_bias:
|
|
|
|
| 117 |
});
|
| 118 |
}
|
| 119 |
|
| 120 |
-
// Decoder
|
| 121 |
let decoder_w = load_matrix(&content, &mut file, "decoder.weight", &device)?;
|
| 122 |
-
let decoder_b =
|
| 123 |
-
|
| 124 |
-
Ok(Self {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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:
|
| 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
|
| 174 |
}
|
| 175 |
// After last layer: hidden is [1, embed_dim]
|
| 176 |
|
| 177 |
-
// 7. Decoder: linear → [
|
| 178 |
-
let coeffs = linear(&hidden, &self.decoder_w, &self.decoder_b, &self.device)?;
|
| 179 |
let n_q = cfg.quantiles.len();
|
| 180 |
-
let
|
| 181 |
-
|
| 182 |
|
| 183 |
-
// Diagnostic: dump all coefficients when FLOWSTATE_DEBUG is set
|
| 184 |
if std::env::var("FLOWSTATE_DEBUG").is_ok() {
|
| 185 |
-
|
| 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.
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
for q in 0..n_q {
|
| 215 |
for p in 0..prediction_length {
|
| 216 |
-
output[q][p] =
|
| 217 |
}
|
| 218 |
}
|
| 219 |
|
|
@@ -226,11 +234,10 @@ impl FlowStateModel {
|
|
| 226 |
|
| 227 |
fn apply_s5_layer(
|
| 228 |
&self,
|
| 229 |
-
x: Tensor, // [seq_len, embed_dim]
|
| 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)? //
|
| 244 |
} else {
|
| 245 |
x.clone()
|
| 246 |
};
|
| 247 |
|
| 248 |
-
// ---- SSM
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
|
| 255 |
// B @ x for all timesteps at once: x [seq_len, embed_dim] × B^T [embed_dim, state_dim]
|
| 256 |
-
|
| 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 |
-
|
| 268 |
-
let
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
}
|
| 279 |
-
|
| 280 |
-
|
| 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
|
| 292 |
};
|
| 293 |
|
| 294 |
-
// C @ h: y_real = C_r @ h_r - C_i @ h_i
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
let
|
| 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 |
-
//
|
| 302 |
-
let
|
| 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
|
| 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 |
-
|
| 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,
|
| 371 |
-
/// B_bar shape
|
| 372 |
fn discretize(
|
| 373 |
ssm: &S5Weights,
|
| 374 |
scale_factor: f32,
|
| 375 |
state_dim: usize,
|
| 376 |
embed_dim: usize,
|
| 377 |
-
|
| 378 |
-
|
| 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;
|
| 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 |
-
(
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 481 |
-
//
|
| 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 |
-
|
| 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: &
|
| 508 |
-
|
| 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
|
| 515 |
fn selu_tensor(x: &Tensor) -> anyhow::Result<Tensor> {
|
| 516 |
const SCALE: f64 = 1.0507009873554804934193349852946;
|
| 517 |
const ALPHA: f64 = 1.6732632423543772848170429916717;
|
| 518 |
-
|
| 519 |
-
let
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 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 |
-
|
| 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
|
| 537 |
-
fn layer_norm(x: &Tensor, weight: &
|
| 538 |
-
let
|
| 539 |
-
let
|
| 540 |
-
let
|
| 541 |
-
let
|
| 542 |
-
|
| 543 |
-
|
| 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
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 228 |
|
| 229 |
-
let mut
|
| 230 |
-
for
|
| 231 |
-
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
}
|
|
|
|
| 234 |
}
|
| 235 |
-
|
| 236 |
-
println!("{}", forecast_json("flowstate-r1",
|
| 237 |
}
|
| 238 |
}
|
| 239 |
|
| 240 |
Ok(())
|
| 241 |
}
|
| 242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
fn forecast_json(
|
| 244 |
model_name: &str,
|
| 245 |
context_length: usize,
|
| 246 |
forecast_length: usize,
|
| 247 |
-
|
| 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:
|
| 287 |
-
index:
|
| 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-
|
| 7 |
version = "0.1.0"
|
| 8 |
-
requires-python = ">=3.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
[tool.
|
| 11 |
-
|
| 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, ®ular, &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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|