File size: 5,401 Bytes
e85df1f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | //! UltraSpeed 资源调度:本机/云端分别限流 + 元数据缓存。
//!
//! # 问题
//!
//! 本机 GPU 推理的并发度很低(一块 GPU 同时跑两个大模型会互相抢显存,
//! 甚至触发 Ollama 卸载重载)。云端 API 则有 RPM/TPM 限流。
//!
//! # 解法
//!
//! 用信号量把**本机并发压到 1**、云端控制在可控并发(默认 2),
//! 请求排队而不是互相踩踏。同时缓存 provider 的 health / models 元数据,
//! 避免每次请求都探活(探活本身也要几十毫秒)。
//!
//! 注意:调度**只影响并发度**,绝不改变模型 / prompt / 思考档 / 回复长度。
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, LazyLock, Mutex};
use std::time::{Duration, Instant};
use tokio::sync::{Mutex as AsyncMutex, OwnedSemaphorePermit, Semaphore};
/// 本机推理并发上限(GPU 单卡最优 = 1,排队比抢显存快)。
pub const LOCAL_CONCURRENCY: usize = 1;
/// 云端并发上限(按你的 API 限额调整)。
pub const CLOUD_CONCURRENCY: usize = 2;
/// 元数据缓存有效期。
pub const METADATA_TTL: Duration = Duration::from_secs(30);
static LOCAL_PERMITS: LazyLock<Arc<Semaphore>> =
LazyLock::new(|| Arc::new(Semaphore::new(LOCAL_CONCURRENCY)));
static CLOUD_PERMITS: LazyLock<Arc<Semaphore>> =
LazyLock::new(|| Arc::new(Semaphore::new(CLOUD_CONCURRENCY)));
/// 申请一个执行许可:`cloud = true` 走云端池,否则走本机池。
///
/// 返回的 permit 释放时自动归还名额。调用方应尽量短持有。
pub async fn acquire(cloud: bool) -> Result<OwnedSemaphorePermit, String> {
let pool = if cloud {
CLOUD_PERMITS.clone()
} else {
LOCAL_PERMITS.clone()
};
pool.acquire_owned()
.await
.map_err(|_| "资源池已关闭".to_string())
}
// ── 凭据版本号:凭据变更后让所有缓存失效 ────────────────────────────
static REVISION: AtomicU64 = AtomicU64::new(0);
/// 当前凭据版本(缓存 key 的一部分)。
pub fn revision() -> u64 {
REVISION.load(Ordering::Relaxed)
}
/// 凭据/配置变更后调用:让所有 provider 的元数据缓存失效。
pub fn invalidate() {
REVISION.fetch_add(1, Ordering::Relaxed);
if let Ok(mut cache) = CACHE.lock() {
cache.clear();
}
}
// ── 带 TTL 的元数据缓存 ────────────────────────────────────────────
struct Metadata {
health: AsyncMutex<Option<(Instant, bool)>>,
models: AsyncMutex<Option<(Instant, Result<Vec<String>, String>)>>,
}
static CACHE: LazyLock<Mutex<HashMap<String, Arc<Metadata>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
fn entry(key: &str) -> Arc<Metadata> {
let mut cache = CACHE.lock().unwrap_or_else(|e| e.into_inner());
cache
.entry(key.to_string())
.or_insert_with(|| {
Arc::new(Metadata {
health: AsyncMutex::new(None),
models: AsyncMutex::new(None),
})
})
.clone()
}
/// 读取健康状态缓存(未命中/过期返回 None)。
pub async fn cached_health(key: &str) -> Option<bool> {
let meta = entry(key);
let guard = meta.health.lock().await;
match guard.as_ref() {
Some((at, value)) if at.elapsed() < METADATA_TTL => Some(*value),
_ => None,
}
}
/// 写入健康状态缓存。
pub async fn store_health(key: &str, healthy: bool) {
let meta = entry(key);
*meta.health.lock().await = Some((Instant::now(), healthy));
}
/// 读取模型列表缓存(未命中/过期返回 None)。
pub async fn cached_models(key: &str) -> Option<Result<Vec<String>, String>> {
let meta = entry(key);
let guard = meta.models.lock().await;
match guard.as_ref() {
Some((at, value)) if at.elapsed() < METADATA_TTL => Some(value.clone()),
_ => None,
}
}
/// 写入模型列表缓存。
pub async fn store_models(key: &str, models: Result<Vec<String>, String>) {
let meta = entry(key);
*meta.models.lock().await = Some((Instant::now(), models));
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn local_pool_serializes_to_one() {
let first = acquire(false).await.unwrap();
// 第二个申请应排队:用 try_acquire 验证名额已满
assert!(
LOCAL_PERMITS.clone().try_acquire_owned().is_err(),
"本机并发必须压到 1(GPU 排队比抢显存快)"
);
drop(first);
assert!(LOCAL_PERMITS.clone().try_acquire_owned().is_ok());
}
#[tokio::test]
async fn metadata_cache_hits_within_ttl_and_invalidates() {
let key = "test-provider";
store_health(key, true).await;
assert_eq!(cached_health(key).await, Some(true));
store_models(key, Ok(vec!["m1".into()])).await;
assert!(cached_models(key).await.is_some());
invalidate();
// invalidate 清空缓存
assert_eq!(cached_health(key).await, None);
assert!(cached_models(key).await.is_none());
}
#[test]
fn revision_monotonically_increases() {
let before = revision();
invalidate();
assert!(revision() > before);
}
}
|