Download src/warmup.rs from hongcoding/perch-gpu: direct link, hf CLI and curl.
- Browser
- Download file 3.04 kB
-
https://huggingface.co/hongcoding/perch-gpu/resolve/main/src/warmup.rs
- Command line
-
hf download hf://hongcoding/perch-gpu/src/warmup.rs
-
curl -L -o warmup.rs https://huggingface.co/hongcoding/perch-gpu/resolve/main/src/warmup.rs
3.04 kB
| //! 热驻留(warm residency):把模型常驻在显存里,避免每次请求冷加载。 | |
| //! | |
| //! # 为什么重要 | |
| //! | |
| //! Ollama 默认会在空闲后卸载模型(free VRAM)。下次请求需要**冷加载 3~5 秒**—— | |
| //! 比云端还慢,混合流的"本机秒回"就名存实亡。 | |
| //! | |
| //! 解法:一次轻量预热 + `keep_alive` 保活。之后每次请求首 token 只要 0.04~0.18 秒。 | |
| //! | |
| //! # 关键:**不要**传 `num_gpu` | |
| //! | |
| //! 强行指定 GPU 层数会触发 runner 重载,反而破坏热驻留。 | |
| //! 让 Ollama 自己选择后端与层数,我们只负责"保持它醒着"。 | |
| use crate::status::DEFAULT_OLLAMA_BASE_URL; | |
| use serde_json::json; | |
| /// 预热请求体(纯函数,可单测)。 | |
| /// | |
| /// - `prompt: "hi"` + `num_predict: 1`——只生成 1 个 token,几乎零开销 | |
| /// - `keep_alive`——保活时长(Ollama 语法:`"30m"` / `"-1"` 永久 / 数字秒) | |
| /// - **不含 `num_gpu`**——见模块文档 | |
| pub fn warmup_body(model: &str, keep_alive: &str) -> serde_json::Value { | |
| json!({ | |
| "model": model, | |
| "prompt": "hi", | |
| "stream": false, | |
| "keep_alive": keep_alive, | |
| "options": { | |
| "num_predict": 1 | |
| } | |
| }) | |
| } | |
| /// 预热一个模型(把请求发到本机 Ollama)。 | |
| /// | |
| /// 这个提示**不会**进入对话历史,也不出本机 loopback。 | |
| pub async fn warmup_model( | |
| model: &str, | |
| keep_alive: &str, | |
| base_url: Option<&str>, | |
| ) -> Result<(), String> { | |
| let model = model.trim(); | |
| if model.is_empty() { | |
| return Err("模型名不能为空".to_string()); | |
| } | |
| if model.len() > 256 { | |
| return Err("模型名过长".to_string()); | |
| } | |
| let base = base_url | |
| .unwrap_or(DEFAULT_OLLAMA_BASE_URL) | |
| .trim_end_matches('/'); | |
| let client = reqwest::Client::builder() | |
| .connect_timeout(std::time::Duration::from_secs(3)) | |
| .build() | |
| .map_err(|e| format!("初始化预热请求失败:{e}"))?; | |
| let response = client | |
| .post(format!("{base}/api/generate")) | |
| .json(&warmup_body(model, keep_alive)) | |
| .timeout(std::time::Duration::from_secs(120)) | |
| .send() | |
| .await | |
| .map_err(|e| format!("预热请求失败:{e}"))?; | |
| if !response.status().is_success() { | |
| return Err(format!("预热返回 {}", response.status())); | |
| } | |
| Ok(()) | |
| } | |
| mod tests { | |
| use super::*; | |
| fn warmup_body_is_local_bounded_and_never_forces_gpu_layers() { | |
| let body = warmup_body("qwen3-06b:latest", "30m"); | |
| assert_eq!(body["model"], "qwen3-06b:latest"); | |
| assert_eq!(body["stream"], false); | |
| assert_eq!(body["keep_alive"], "30m"); | |
| assert_eq!(body["options"]["num_predict"], 1); | |
| // 红线:绝不强行指定 GPU 层数(会触发 runner 重载) | |
| assert!(body["options"].get("num_gpu").is_none()); | |
| } | |
| async fn rejects_empty_model_name() { | |
| assert!(warmup_model("", "30m", None).await.is_err()); | |
| } | |
| } | |