chore(release): v0.2.25 — direct stream download for ready CMF repos
Docker / publish (push) Canceled after 0s
Release / crates-io (push) Canceled after 0s
Release / release (macos-14, aarch64-apple-darwin) (push) Canceled after 0s
Release / release (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Canceled after 0s
Release / release (windows-latest, x86_64-pc-windows-msvc) (push) Canceled after 0s
Docker / publish (push) Canceled after 0s
Release / crates-io (push) Canceled after 0s
Release / release (macos-14, aarch64-apple-darwin) (push) Canceled after 0s
Release / release (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Canceled after 0s
Release / release (windows-latest, x86_64-pc-windows-msvc) (push) Canceled after 0s
This commit is contained in:
+5
-1
@@ -3,7 +3,11 @@
|
|||||||
All notable changes to this project are documented here.
|
All notable changes to this project are documented here.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
||||||
|
|
||||||
## [0.2.24] - 2026-07-21
|
## [0.2.25] - 2026-07-21
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Direct Ready CMF Download**: Importing pre-converted `.cmf` model repositories (e.g. `infosave/Bonsai-8B_2bit_cmf`) directly streams the ready `.cmf` file from Hugging Face instead of attempting a safetensors conversion without `config.json`.
|
||||||
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **New Quantization Schemes**: Added support for `Q4_TILED` (q4t), `Q1T` (1.58-bit ternary), `Q1S` (1-bit sparse), `Q1P` (1-bit PTQ error diffusion), and `Q1` (pure binary) in converter backend and web admin UI.
|
- **New Quantization Schemes**: Added support for `Q4_TILED` (q4t), `Q1T` (1.58-bit ternary), `Q1S` (1-bit sparse), `Q1P` (1-bit PTQ error diffusion), and `Q1` (pure binary) in converter backend and web admin UI.
|
||||||
|
|||||||
Generated
+1
-1
@@ -285,7 +285,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cortiq-gateway"
|
name = "cortiq-gateway"
|
||||||
version = "0.2.24"
|
version = "0.2.25"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cortiq-gateway"
|
name = "cortiq-gateway"
|
||||||
version = "0.2.24"
|
version = "0.2.25"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.75"
|
rust-version = "1.75"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
+171
@@ -273,6 +273,161 @@ fn map_quant(q: &str) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_cmf_repo(repo: &str) -> bool {
|
||||||
|
let lower = repo.to_ascii_lowercase();
|
||||||
|
lower.contains("cmf") || lower.ends_with(".cmf")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn download_cmf_repo(
|
||||||
|
repo: String,
|
||||||
|
output_abs: String,
|
||||||
|
hf_token: Option<String>,
|
||||||
|
store: Arc<JobStore>,
|
||||||
|
id: String,
|
||||||
|
cancel: Arc<Notify>,
|
||||||
|
) {
|
||||||
|
store.push_line(&id, format!("→ downloading ready .cmf model from {}", repo));
|
||||||
|
store.set_progress(&id, 0.0, "listing files".into());
|
||||||
|
|
||||||
|
let client = match reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
store.push_line(&id, format!("✗ failed to build HTTP client: {e}"));
|
||||||
|
store.finish(&id, false);
|
||||||
|
store.drop_cancel(&id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let tree_url = format!("https://huggingface.co/api/models/{repo}/tree/main?recursive=true");
|
||||||
|
let mut req = client.get(&tree_url).header("User-Agent", "cortiq-gateway");
|
||||||
|
if let Some(t) = &hf_token {
|
||||||
|
req = req.bearer_auth(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cmf_rel_path = None;
|
||||||
|
if let Ok(resp) = req.send().await {
|
||||||
|
if resp.status().is_success() {
|
||||||
|
if let Ok(json) = resp.json::<serde_json::Value>().await {
|
||||||
|
if let Some(arr) = json.as_array() {
|
||||||
|
for item in arr {
|
||||||
|
if let Some(path) = item.get("path").and_then(|p| p.as_str()) {
|
||||||
|
if path.to_ascii_lowercase().ends_with(".cmf") {
|
||||||
|
cmf_rel_path = Some(path.to_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cmf_path = match cmf_rel_path {
|
||||||
|
Some(p) => p,
|
||||||
|
None => {
|
||||||
|
let repo_name = repo.split('/').last().unwrap_or(&repo);
|
||||||
|
format!("{repo_name}.cmf")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
store.push_line(&id, format!("→ downloading {cmf_path} from HuggingFace..."));
|
||||||
|
store.set_progress(&id, 0.05, "downloading".into());
|
||||||
|
|
||||||
|
let download_url = format!("https://huggingface.co/{repo}/resolve/main/{cmf_path}");
|
||||||
|
let mut req = client
|
||||||
|
.get(&download_url)
|
||||||
|
.header("User-Agent", "cortiq-gateway");
|
||||||
|
if let Some(t) = &hf_token {
|
||||||
|
req = req.bearer_auth(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut resp = match req.send().await {
|
||||||
|
Ok(r) if r.status().is_success() => r,
|
||||||
|
Ok(r) => {
|
||||||
|
store.push_line(&id, format!("✗ HF download error: HTTP {}", r.status()));
|
||||||
|
store.finish(&id, false);
|
||||||
|
store.drop_cancel(&id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
store.push_line(&id, format!("✗ network error: {e}"));
|
||||||
|
store.finish(&id, false);
|
||||||
|
store.drop_cancel(&id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let total_bytes = resp.content_length();
|
||||||
|
let mut file = match tokio::fs::File::create(&output_abs).await {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => {
|
||||||
|
store.push_line(&id, format!("✗ failed to create file {output_abs}: {e}"));
|
||||||
|
store.finish(&id, false);
|
||||||
|
store.drop_cancel(&id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
let mut downloaded: u64 = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancel.notified() => {
|
||||||
|
let _ = tokio::fs::remove_file(&output_abs).await;
|
||||||
|
store.push_line(&id, "✗ cancelled by user — partial output removed".into());
|
||||||
|
store.set_state(&id, "cancelled");
|
||||||
|
store.drop_cancel(&id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chunk_opt = resp.chunk() => {
|
||||||
|
match chunk_opt {
|
||||||
|
Ok(Some(chunk)) => {
|
||||||
|
if let Err(e) = file.write_all(&chunk).await {
|
||||||
|
store.push_line(&id, format!("✗ write error: {e}"));
|
||||||
|
cleanup_output(&output_abs);
|
||||||
|
store.finish(&id, false);
|
||||||
|
store.drop_cancel(&id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
downloaded += chunk.len() as u64;
|
||||||
|
if let Some(total) = total_bytes {
|
||||||
|
if total > 0 {
|
||||||
|
let frac = (downloaded as f32 / total as f32).clamp(0.0, 1.0);
|
||||||
|
store.set_progress(&id, frac, "downloading".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => break,
|
||||||
|
Err(e) => {
|
||||||
|
store.push_line(&id, format!("✗ download stream error: {e}"));
|
||||||
|
cleanup_output(&output_abs);
|
||||||
|
store.finish(&id, false);
|
||||||
|
store.drop_cancel(&id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = file.flush().await {
|
||||||
|
store.push_line(&id, format!("✗ flush error: {e}"));
|
||||||
|
cleanup_output(&output_abs);
|
||||||
|
store.finish(&id, false);
|
||||||
|
store.drop_cancel(&id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.push_line(&id, "✓ done".into());
|
||||||
|
store.finish(&id, true);
|
||||||
|
store.drop_cancel(&id);
|
||||||
|
}
|
||||||
|
|
||||||
/// into the job log. Fails fast if the converter script is missing.
|
/// into the job log. Fails fast if the converter script is missing.
|
||||||
pub fn start_import(store: Arc<JobStore>, cfg: &CmfCfg, p: ImportParams) -> Result<String, String> {
|
pub fn start_import(store: Arc<JobStore>, cfg: &CmfCfg, p: ImportParams) -> Result<String, String> {
|
||||||
if p.repo.trim().is_empty() {
|
if p.repo.trim().is_empty() {
|
||||||
@@ -353,6 +508,14 @@ pub fn start_import(store: Arc<JobStore>, cfg: &CmfCfg, p: ImportParams) -> Resu
|
|||||||
std::env::var(&cfg.hf_token_env).ok()
|
std::env::var(&cfg.hf_token_env).ok()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if is_cmf_repo(&p.repo) && !use_python {
|
||||||
|
let ret_id = id.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
download_cmf_repo(p.repo, output_abs, hf_token, store, id, cancel).await;
|
||||||
|
});
|
||||||
|
return Ok(ret_id);
|
||||||
|
}
|
||||||
|
|
||||||
// Program + args: `cortiq convert` by default, or the Python converter for
|
// Program + args: `cortiq convert` by default, or the Python converter for
|
||||||
// advanced options. Both stream `@PROGRESS <frac>` markers into the log.
|
// advanced options. Both stream `@PROGRESS <frac>` markers into the log.
|
||||||
let (program, args, workdir): (String, Vec<String>, std::path::PathBuf) = if use_python {
|
let (program, args, workdir): (String, Vec<String>, std::path::PathBuf) = if use_python {
|
||||||
@@ -555,4 +718,12 @@ mod tests {
|
|||||||
assert_eq!(map_quant("Q1T"), "q1t");
|
assert_eq!(map_quant("Q1T"), "q1t");
|
||||||
assert_eq!(map_quant("F16"), "f16");
|
assert_eq!(map_quant("F16"), "f16");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_cmf_repo_check() {
|
||||||
|
assert!(is_cmf_repo("infosave/Bonsai-8B_2bit_cmf"));
|
||||||
|
assert!(is_cmf_repo("infosave/Bonsai-1.7Bcmf"));
|
||||||
|
assert!(is_cmf_repo("user/model.cmf"));
|
||||||
|
assert!(!is_cmf_repo("Qwen/Qwen2.5-0.5B-Instruct"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user