mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 06:28:11 +02:00
Lot 25 : Strata tenu par la façade (LOKI_STRATA=rust)
Port de backend_strata.go (rust/loki-rs/src/strata) : la machine (cartes par nvidia-smi, Python 3.10+ et venv, RAM totale et disponible, RAM du moteur Strata en marche, place libre), ce qui y tient et la recommandation, la configuration de lancement en clair des modèles installés (placement des experts drop/ram/arena/disk, carte d'aide, réglages du preset), les réglages d'un modèle installé et l'installation (job des moteurs : paquet figé vérifié par SHA-256, installeur lancé et journalisé comme le sinkWriter du Go, preset écrit puis activé par le Go). Tenu seulement avec LOKI_BACKENDS=rust ; le lancement (loki serve) reste au Go, qui relaie les routes du tunnel. Correction du Go : /api/strata/settings décidait « en service » après avoir écrit les nouveaux réglages, donc jamais pour un réglage changé ; décidé désormais avant l'écriture, comme SavePresetApplying. Le port suit. Parité : parity_strata_test.go (crochets de test : release, meminfo, place libre, action de service) tire du Go un journal rejoué par tests/strata.rs ; les tests du Go portés dans strata/tests.rs ; TestFacadeStrata côté Go. Mutations : 71 tuées sur 71. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
22 files changed
+9079
-19
No files matched your search
@@ -0,0 +1,848 @@
|
||||
//! Strata, deuxième moteur de Loki (lot 25 de la migration,
|
||||
//! `LOKI_STRATA=rust` ; expérimental, le Go reste le défaut) — port de
|
||||
//! `backend_strata.go`.
|
||||
//!
|
||||
//! Strata répartit les experts d'un MoE de 125 B (famille Qwen3.8-Flash-Next)
|
||||
//! entre VRAM, RAM et disque. Le moteur est le paquet FIGÉ publié par AJEAN
|
||||
//! (sources + binaire Linux CUDA 12, vérifiés par SHA-256) ; le choix se fait
|
||||
//! par preset (`ENGINE=strata`).
|
||||
//!
|
||||
//! Ici : la décision pure (placement des experts, configuration de lancement,
|
||||
//! ce qui tient sur la machine, recommandation, réglages), la machine (cartes,
|
||||
//! RAM, disque, Python) et les chemins. Les routes et l'installation sont dans
|
||||
//! [`api`].
|
||||
|
||||
pub mod api;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
/// Paquet figé (release moe-v1.0 d'AJEAN).
|
||||
pub const VERSION: &str = "1.0";
|
||||
pub const RELEASE_BASE: &str = "https://github.com/nathaninline/ajean/releases/download/moe-v1.0/";
|
||||
pub const SRC_ASSET: &str = "ajean-moe-src-1.0.tar.gz";
|
||||
pub const ENGINE_ASSET: &str = "ajean-moe-engine-1.0-linux-x64-cuda12.zip";
|
||||
/// Le nom sous lequel l'installeur attend le moteur (CUDA12_ASSET).
|
||||
pub const ENGINE_LOCAL: &str = "strata-linux-x64-cuda12.zip";
|
||||
|
||||
/// `strataAssetSHA`.
|
||||
pub fn default_asset_sha() -> HashMap<String, String> {
|
||||
HashMap::from([
|
||||
(
|
||||
SRC_ASSET.to_string(),
|
||||
"5747f69426973d957e4f9894a8e3333c6ef0d6ac8565e26ddb8d36f52211cb3b".to_string(),
|
||||
),
|
||||
(
|
||||
ENGINE_ASSET.to_string(),
|
||||
"b5851bc138ab32ca3ffefc16bf91c8f557e4d503503473a22f2b6c9e9b8fd34c".to_string(),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
pub const ENGINE_LLAMACPP: &str = "llamacpp";
|
||||
pub const ENGINE_STRATA: &str = "strata";
|
||||
|
||||
pub type Cfg = HashMap<String, String>;
|
||||
|
||||
fn cfg_get<'a>(cfg: &'a Cfg, k: &str) -> &'a str {
|
||||
cfg.get(k).map_or("", String::as_str)
|
||||
}
|
||||
|
||||
/// `engineOf` : « moe » est accepté pour un preset recopié d'AJEAN.
|
||||
pub fn engine_of(cfg: &Cfg) -> &'static str {
|
||||
match cfg_get(cfg, "ENGINE").trim().to_lowercase().as_str() {
|
||||
ENGINE_STRATA | "moe" => ENGINE_STRATA,
|
||||
_ => ENGINE_LLAMACPP,
|
||||
}
|
||||
}
|
||||
|
||||
/// `isStrataConfig`.
|
||||
pub fn is_strata(cfg: &Cfg) -> bool {
|
||||
engine_of(cfg) == ENGINE_STRATA
|
||||
}
|
||||
|
||||
/// `strataFamily`.
|
||||
pub struct Family {
|
||||
pub id: &'static str,
|
||||
pub label: &'static str,
|
||||
pub about: &'static str,
|
||||
pub quants: &'static [&'static str],
|
||||
}
|
||||
|
||||
/// `strataQuant`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Quant {
|
||||
pub id: &'static str,
|
||||
pub about: &'static str,
|
||||
pub download_gb: f64,
|
||||
pub arena_gb: f64,
|
||||
}
|
||||
|
||||
impl Quant {
|
||||
fn to_json(self) -> Value {
|
||||
json!({
|
||||
"id": self.id, "about": self.about,
|
||||
"download_gb": go_num(self.download_gb), "arena_gb": go_num(self.arena_gb),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub const FAMILIES: &[Family] = &[
|
||||
Family {
|
||||
id: "swift",
|
||||
label: "Swift 1.5",
|
||||
about: "réfléchit moins, répond plus vite",
|
||||
quants: &["IQ2_XS", "IQ3_XXS"],
|
||||
},
|
||||
Family {
|
||||
id: "qwen",
|
||||
label: "Classique",
|
||||
about: "le Qwen3.8-Flash-Next original",
|
||||
quants: &["Q2_0", "IQ2_XS", "IQ3_XXS", "IQ3_S"],
|
||||
},
|
||||
];
|
||||
|
||||
const QUANTS: &[Quant] = &[
|
||||
Quant {
|
||||
id: "Q2_0",
|
||||
about: "le plus rapide",
|
||||
download_gb: 66.4,
|
||||
arena_gb: 34.0,
|
||||
},
|
||||
Quant {
|
||||
id: "IQ2_XS",
|
||||
about: "un peu plus fidèle, presque aussi rapide",
|
||||
download_gb: 68.0,
|
||||
arena_gb: 35.5,
|
||||
},
|
||||
Quant {
|
||||
id: "IQ3_XXS",
|
||||
about: "99 % de la qualité du modèle complet",
|
||||
download_gb: 75.8,
|
||||
arena_gb: 42.9,
|
||||
},
|
||||
Quant {
|
||||
id: "IQ3_S",
|
||||
about: "la meilleure qualité, le plus lent",
|
||||
download_gb: 83.6,
|
||||
arena_gb: 50.3,
|
||||
},
|
||||
];
|
||||
|
||||
/// `strataQuants[id]`.
|
||||
pub fn quant(id: &str) -> Option<Quant> {
|
||||
QUANTS.iter().find(|q| q.id == id).copied()
|
||||
}
|
||||
|
||||
pub const LOW_RAM_HEADROOM_GB: f64 = 10.0;
|
||||
pub const DROP_HEADROOM_GB: f64 = 7.0;
|
||||
|
||||
/// `strataHelperExpertsGB`.
|
||||
pub fn helper_experts_gb(vram_gb: f64) -> f64 {
|
||||
(vram_gb - 2.5).max(0.0)
|
||||
}
|
||||
|
||||
pub const EXPERTS_AUTO: &str = "auto";
|
||||
pub const EXPERTS_RAM: &str = "ram";
|
||||
pub const EXPERTS_DISK: &str = "disk";
|
||||
|
||||
/// `strataMaxCtx`.
|
||||
pub const MAX_CTX: i64 = 262144;
|
||||
/// `strataMinArch`.
|
||||
pub const MIN_ARCH: i64 = 75;
|
||||
/// `strataLaunchModeKey` (bkState).
|
||||
pub const LAUNCH_MODE_KEY: &str = "strata_experts_mode";
|
||||
/// `strataServeMarker`.
|
||||
pub const SERVE_MARKER: &str = "[loki serve] Strata";
|
||||
|
||||
/// Un nombre comme le Go l'écrit en JSON (entier sans « .0 »).
|
||||
pub fn go_num(f: f64) -> Value {
|
||||
if f.fract() == 0.0 && f.abs() < 1e15 {
|
||||
json!(f as i64)
|
||||
} else {
|
||||
json!(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// `fmt.Sprint` d'une valeur JSON décodée par le Go (`any`).
|
||||
pub fn go_sprint(v: &Value) -> String {
|
||||
match v {
|
||||
Value::String(s) => s.clone(),
|
||||
Value::Number(n) => go_g(n.as_f64().unwrap_or(0.0)),
|
||||
Value::Bool(b) => b.to_string(),
|
||||
Value::Null => "<nil>".into(),
|
||||
Value::Array(a) => format!(
|
||||
"[{}]",
|
||||
a.iter().map(go_sprint).collect::<Vec<_>>().join(" ")
|
||||
),
|
||||
Value::Object(m) => format!(
|
||||
"map[{}]",
|
||||
m.iter()
|
||||
.map(|(k, v)| format!("{k}:{}", go_sprint(v)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// `strconv.FormatFloat(f, 'g', -1, 64)` (le `%v` d'un float64) : le plus
|
||||
/// court, en notation exponentielle hors de [1e-4, 1e6[.
|
||||
pub fn go_g(f: f64) -> String {
|
||||
if f.is_nan() {
|
||||
return "NaN".into();
|
||||
}
|
||||
if f.is_infinite() {
|
||||
return if f > 0.0 { "+Inf" } else { "-Inf" }.into();
|
||||
}
|
||||
if f == 0.0 {
|
||||
return if f.is_sign_negative() { "-0" } else { "0" }.into();
|
||||
}
|
||||
let e = format!("{f:e}");
|
||||
let (mant, exp) = e.split_once('e').unwrap_or((&e, "0"));
|
||||
let exp: i32 = exp.parse().unwrap_or(0);
|
||||
if !(-4..6).contains(&exp) {
|
||||
let sign = if exp < 0 { '-' } else { '+' };
|
||||
return format!("{mant}e{sign}{:02}", exp.abs());
|
||||
}
|
||||
format!("{f}")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Chemins
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Les chemins de Strata sous `$LOKI_HOME`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Paths {
|
||||
pub home: PathBuf,
|
||||
}
|
||||
|
||||
impl Paths {
|
||||
pub fn strata_home(&self) -> PathBuf {
|
||||
self.home.join("strata")
|
||||
}
|
||||
pub fn src_dir(&self) -> PathBuf {
|
||||
self.strata_home().join(format!("ajean-moe-{VERSION}"))
|
||||
}
|
||||
pub fn data_dir(&self) -> PathBuf {
|
||||
self.strata_home().join("data")
|
||||
}
|
||||
pub fn dl_dir(&self) -> PathBuf {
|
||||
self.strata_home().join("dl")
|
||||
}
|
||||
pub fn presets_dir(&self) -> PathBuf {
|
||||
self.home.join("presets")
|
||||
}
|
||||
/// `strataEngineMarker` : le paquet dont vient le moteur installé.
|
||||
pub fn engine_marker(&self) -> PathBuf {
|
||||
self.src_dir().join("engine-cuda12").join(".loki-engine")
|
||||
}
|
||||
/// `strataEngineInstalled`.
|
||||
pub fn engine_installed(&self) -> String {
|
||||
std::fs::read_to_string(self.engine_marker())
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Experts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `strataConfigTag` : strata-swift-iq3_xxs.json → swift-iq3_xxs.
|
||||
pub fn config_tag(path: &str) -> String {
|
||||
let b = crate::paths::go_base(path);
|
||||
let b = b.strip_suffix(".json").unwrap_or(&b);
|
||||
match b.find('-') {
|
||||
Some(i) => b[i + 1..].to_string(),
|
||||
None => b.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `strataArenaGB` : la taille des experts du quant d'un preset (0 = inconnu).
|
||||
pub fn arena_gb(cfg: &Cfg) -> f64 {
|
||||
let tag = config_tag(cfg_get(cfg, "STRATA_CONFIG").trim()).to_uppercase();
|
||||
let tag = tag.strip_prefix("SWIFT-").unwrap_or(&tag);
|
||||
quant(tag).map_or(0.0, |q| q.arena_gb)
|
||||
}
|
||||
|
||||
/// `strataPackHasExpertsBin`.
|
||||
pub fn pack_has_experts_bin(args: &[String]) -> bool {
|
||||
args.windows(2)
|
||||
.any(|w| w[0] == "--pack" && Path::new(&w[1]).join("experts.bin").exists())
|
||||
}
|
||||
|
||||
/// `strataHelperOn` : la carte d'aide existe et n'a pas été coupée.
|
||||
pub fn helper_on(cfg: &Cfg) -> bool {
|
||||
let h = cfg_get(cfg, "STRATA_HELPER_GPU").trim();
|
||||
!h.is_empty() && h != "-1" && cfg_get(cfg, "STRATA_HELPER") != "0"
|
||||
}
|
||||
|
||||
/// `strataResolveExperts` : le mode lancé (drop, ram, arena, disk), d'après
|
||||
/// le choix du preset et la RAM disponible.
|
||||
pub fn resolve_experts(
|
||||
paths: &Paths,
|
||||
cfg: &Cfg,
|
||||
args: &[String],
|
||||
avail_gb: f64,
|
||||
helper_gb: f64,
|
||||
) -> &'static str {
|
||||
let bin_ready = pack_has_experts_bin(args) && paths.engine_installed() == ENGINE_ASSET;
|
||||
let drop_ready = bin_ready && helper_on(cfg) && cfg_get(cfg, "STRATA_DROP") != "0";
|
||||
let arena = arena_gb(cfg);
|
||||
match cfg_get(cfg, "STRATA_EXPERTS")
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
EXPERTS_DISK => return if bin_ready { "arena" } else { "disk" },
|
||||
EXPERTS_RAM => return if drop_ready { "drop" } else { "ram" },
|
||||
_ => {}
|
||||
}
|
||||
if arena <= 0.0 {
|
||||
return "disk";
|
||||
}
|
||||
if drop_ready && avail_gb >= arena - helper_experts_gb(helper_gb) + DROP_HEADROOM_GB {
|
||||
return "drop";
|
||||
}
|
||||
if avail_gb >= arena + LOW_RAM_HEADROOM_GB {
|
||||
return "ram";
|
||||
}
|
||||
if bin_ready { "arena" } else { "disk" }
|
||||
}
|
||||
|
||||
/// `strataExpertsLabel`.
|
||||
pub fn experts_label(mode: &str) -> String {
|
||||
match mode {
|
||||
"drop" => "verrouillés en RAM, hors ceux de la carte d'appoint".into(),
|
||||
"ram" => "chargés et verrouillés en RAM".into(),
|
||||
"arena" => "experts.bin mappé (cache système)".into(),
|
||||
"disk" => "lus depuis le disque (mmap)".into(),
|
||||
m => m.into(),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Configuration de lancement
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `strataTuning` (ordre des clés de la map Go, itérée au hasard mais
|
||||
/// idempotente : chaque drapeau est posé une fois).
|
||||
const TUNING: [(&str, &str); 3] = [
|
||||
("--prefill", "16384"),
|
||||
("--short-read", "150"),
|
||||
("--prompt-cache-root", "256"),
|
||||
];
|
||||
|
||||
/// `strataEnvTuning`.
|
||||
const ENV_TUNING: [(&str, &str); 2] = [("STRATA_PLE_BATCH", "0"), ("STRATA_NO_LARGEPAGES", "1")];
|
||||
|
||||
/// `strataSetArg`.
|
||||
pub fn set_arg(mut args: Vec<String>, flag: &str, value: &str) -> Vec<String> {
|
||||
for i in 0..args.len().saturating_sub(1) {
|
||||
if args[i] == flag {
|
||||
args[i + 1] = value.to_string();
|
||||
return args;
|
||||
}
|
||||
}
|
||||
args.push(flag.to_string());
|
||||
args.push(value.to_string());
|
||||
args
|
||||
}
|
||||
|
||||
/// `strataDropFlag` : retire un drapeau SANS valeur.
|
||||
pub fn drop_flag(args: Vec<String>, flag: &str) -> Vec<String> {
|
||||
args.into_iter().filter(|a| a != flag).collect()
|
||||
}
|
||||
|
||||
/// `strataDropFlagValue` : retire un drapeau ET sa valeur.
|
||||
pub fn drop_flag_value(args: Vec<String>, flag: &str) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
if args[i] == flag {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
out.push(args[i].clone());
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `strataHasArg`.
|
||||
pub fn has_arg(args: &[String], flag: &str) -> bool {
|
||||
args.iter().any(|a| a == flag)
|
||||
}
|
||||
|
||||
/// `strataArgList`.
|
||||
pub fn arg_list(cfg: &Value) -> Vec<String> {
|
||||
cfg.get("args")
|
||||
.and_then(Value::as_array)
|
||||
.map(|l| l.iter().map(go_sprint).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn first_non_empty<'a>(a: &'a str, b: &'a str) -> &'a str {
|
||||
if a.is_empty() { b } else { a }
|
||||
}
|
||||
|
||||
/// `strataBuildConfig` : la config de l'installeur avec les réglages du
|
||||
/// preset et les experts placés selon `mode`. Pure.
|
||||
pub fn build_config(base: &Value, cfg: &Cfg, api_key: &str, mode: &str) -> Result<Value, String> {
|
||||
let mut out: Map<String, Value> = match base {
|
||||
Value::Object(m) => m.clone(),
|
||||
_ => Map::new(),
|
||||
};
|
||||
if !out.get("args").is_some_and(Value::is_array) {
|
||||
return Err("configuration de Strata sans « args »".into());
|
||||
}
|
||||
let mut args = arg_list(&Value::Object(out.clone()));
|
||||
for (flag, v) in TUNING {
|
||||
args = set_arg(args, flag, v);
|
||||
}
|
||||
let helper = cfg_get(cfg, "STRATA_HELPER_GPU").trim();
|
||||
let has_helper = helper_on(cfg);
|
||||
if has_helper {
|
||||
args = set_arg(args, "--expert-cache-device1", "auto");
|
||||
if !has_arg(&args, "--remote-expert-opt") {
|
||||
args.push("--remote-expert-opt".into());
|
||||
}
|
||||
out.remove("gpu");
|
||||
args = drop_flag_value(args, "--vram-reserve-mib");
|
||||
if let Some(Value::Object(v)) = out.get_mut("vision")
|
||||
&& let Ok(n) = helper.parse::<i64>()
|
||||
{
|
||||
v.insert("cuda_device".into(), json!(n));
|
||||
}
|
||||
}
|
||||
let mut env: Map<String, Value> = match out.get("env") {
|
||||
Some(Value::Object(e)) => e.clone(),
|
||||
_ => Map::new(),
|
||||
};
|
||||
args = drop_flag(drop_flag(args, "--mmap-experts"), "--resident-experts");
|
||||
env.remove("STRATA_ARENA_MMAP");
|
||||
env.remove("STRATA_REMOTE_DROP");
|
||||
match mode {
|
||||
"drop" if has_helper => {
|
||||
args = drop_flag_value(args, "--pcie-frac");
|
||||
env.insert("STRATA_REMOTE_DROP".into(), json!("1"));
|
||||
env.insert("STRATA_STAGER_THREADS".into(), json!("8"));
|
||||
env.insert("STRATA_STAGER_RING".into(), json!("32"));
|
||||
}
|
||||
"arena" => {
|
||||
args = set_arg(args, "--pcie-frac", "0");
|
||||
env.insert("STRATA_ARENA_MMAP".into(), json!("1"));
|
||||
}
|
||||
"disk" => {
|
||||
args.push("--mmap-experts".into());
|
||||
args = set_arg(args, "--pcie-frac", "0");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
for (k, v) in ENV_TUNING {
|
||||
env.insert(k.into(), json!(v));
|
||||
}
|
||||
out.insert("env".into(), Value::Object(env));
|
||||
let ctx = cfg_get(cfg, "CTX").trim();
|
||||
if !ctx.is_empty() {
|
||||
args = set_arg(args, "--max-context", ctx);
|
||||
}
|
||||
for (key, flag) in [
|
||||
("STRATA_KV", "--kv"),
|
||||
("STRATA_SPEC", "--spec"),
|
||||
("STRATA_KV_RESIDENT", "--kv-resident"),
|
||||
] {
|
||||
let v = cfg_get(cfg, key).trim();
|
||||
if !v.is_empty() {
|
||||
args = set_arg(args, flag, v);
|
||||
}
|
||||
}
|
||||
if cfg_get(cfg, "STRATA_VISION") == "0" {
|
||||
args = drop_flag(args, "--vision");
|
||||
out.remove("vision");
|
||||
}
|
||||
out.insert(
|
||||
"args".into(),
|
||||
Value::Array(args.into_iter().map(Value::String).collect()),
|
||||
);
|
||||
let port = first_non_empty(cfg_get(cfg, "PORT"), "8080")
|
||||
.parse::<i64>()
|
||||
.unwrap_or(0);
|
||||
out.insert("port".into(), json!(port));
|
||||
out.insert(
|
||||
"host".into(),
|
||||
json!(first_non_empty(cfg_get(cfg, "HOST"), "127.0.0.1")),
|
||||
);
|
||||
if !api_key.is_empty() {
|
||||
out.insert("api_key".into(), json!(api_key));
|
||||
}
|
||||
Ok(Value::Object(out))
|
||||
}
|
||||
|
||||
/// `strataCudaDevices` : la principale en CUDA0.
|
||||
pub fn cuda_devices(cfg: &Cfg) -> String {
|
||||
let main = cfg_get(cfg, "STRATA_MAIN_GPU").trim();
|
||||
if main.is_empty() || main == "-1" {
|
||||
return String::new();
|
||||
}
|
||||
if helper_on(cfg) {
|
||||
format!("{main},{}", cfg_get(cfg, "STRATA_HELPER_GPU").trim())
|
||||
} else {
|
||||
main.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// `strataPreflight` : ce qu'il faut pour lancer Strata.
|
||||
pub fn preflight(cfg: &Cfg) -> Result<(), String> {
|
||||
let path = cfg_get(cfg, "STRATA_CONFIG").trim();
|
||||
if path.is_empty() {
|
||||
return Err(
|
||||
"STRATA_CONFIG non défini : réinstalle le modèle depuis Paramètres → Moteur → Strata"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if std::fs::metadata(path).is_err() {
|
||||
return Err(format!(
|
||||
"configuration de Strata introuvable : {path} — réinstalle le modèle"
|
||||
));
|
||||
}
|
||||
let py = Path::new(&crate::paths::go_dir(path))
|
||||
.join(".venv")
|
||||
.join("bin")
|
||||
.join("python");
|
||||
if std::fs::metadata(&py).is_err() {
|
||||
return Err(format!(
|
||||
"environnement Python de Strata absent ({}) : réinstalle le modèle",
|
||||
py.to_string_lossy()
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Machine
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `strataGPU`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Gpu {
|
||||
pub index: i64,
|
||||
pub name: String,
|
||||
pub vram_gb: f64,
|
||||
pub arch: i64,
|
||||
}
|
||||
|
||||
impl Gpu {
|
||||
fn to_json(&self) -> Value {
|
||||
json!({ "index": self.index, "name": self.name, "vram_gb": go_num(self.vram_gb), "arch": self.arch })
|
||||
}
|
||||
}
|
||||
|
||||
/// `strataEnv`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Env {
|
||||
pub supported: bool,
|
||||
pub reason: String,
|
||||
pub gpus: Vec<Gpu>,
|
||||
pub main: i64,
|
||||
pub helper: i64,
|
||||
pub ram_gb: f64,
|
||||
pub avail_gb: f64,
|
||||
pub disk_free_gb: f64,
|
||||
pub python: String,
|
||||
}
|
||||
|
||||
impl Default for Env {
|
||||
fn default() -> Self {
|
||||
Env {
|
||||
supported: false,
|
||||
reason: String::new(),
|
||||
gpus: Vec::new(),
|
||||
main: -1,
|
||||
helper: -1,
|
||||
ram_gb: 0.0,
|
||||
avail_gb: 0.0,
|
||||
disk_free_gb: 0.0,
|
||||
python: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Env {
|
||||
pub fn to_json(&self) -> Value {
|
||||
let mut v = json!({
|
||||
"supported": self.supported,
|
||||
"gpus": if self.gpus.is_empty() { Value::Null } else { Value::Array(self.gpus.iter().map(Gpu::to_json).collect()) },
|
||||
"main": self.main, "helper": self.helper,
|
||||
"ram_gb": go_num(self.ram_gb), "avail_gb": go_num(self.avail_gb),
|
||||
"disk_free_gb": go_num(self.disk_free_gb),
|
||||
});
|
||||
if !self.reason.is_empty() {
|
||||
v["reason"] = json!(self.reason);
|
||||
}
|
||||
if !self.python.is_empty() {
|
||||
v["python"] = json!(self.python);
|
||||
}
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
/// `strataTag` : « swift-IQ3_XXS », « IQ3_XXS ».
|
||||
pub fn tag(family: &str, quant: &str) -> String {
|
||||
if family == "swift" {
|
||||
format!("swift-{quant}")
|
||||
} else {
|
||||
quant.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// `strataDirGB` : la taille des fichiers d'un dossier, en Go.
|
||||
pub fn dir_gb(dir: &Path) -> f64 {
|
||||
fn walk(p: &Path) -> u64 {
|
||||
let Ok(rd) = std::fs::read_dir(p) else {
|
||||
return 0;
|
||||
};
|
||||
let mut t = 0;
|
||||
for e in rd.flatten() {
|
||||
let Ok(ft) = e.file_type() else { continue };
|
||||
if ft.is_dir() {
|
||||
t += walk(&e.path());
|
||||
} else if ft.is_file() {
|
||||
t += e.metadata().map_or(0, |m| m.len());
|
||||
}
|
||||
}
|
||||
t
|
||||
}
|
||||
walk(dir) as f64 / 1e9
|
||||
}
|
||||
|
||||
/// `strataHaveGB` : ce qui est déjà sur le disque pour CE choix.
|
||||
pub fn have_gb(paths: &Paths, family: &str, quant: &str) -> f64 {
|
||||
let t = tag(family, quant);
|
||||
let data = paths.data_dir();
|
||||
dir_gb(&data.join("models").join(&t))
|
||||
+ dir_gb(&data.join("packs").join(t.to_lowercase()))
|
||||
+ dir_gb(&data.join("mtp"))
|
||||
}
|
||||
|
||||
/// `strataFit`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Fit {
|
||||
pub quant: Quant,
|
||||
pub disk_gb: f64,
|
||||
pub mmap: bool,
|
||||
pub drop: bool,
|
||||
pub ok: bool,
|
||||
pub why: String,
|
||||
pub preset: String,
|
||||
pub active: bool,
|
||||
pub details: Option<Value>,
|
||||
}
|
||||
|
||||
impl Fit {
|
||||
pub fn to_json(&self) -> Value {
|
||||
let mut v = json!({
|
||||
"quant": self.quant.to_json(), "disk_gb": go_num(self.disk_gb),
|
||||
"mmap": self.mmap, "drop": self.drop, "ok": self.ok,
|
||||
});
|
||||
if !self.why.is_empty() {
|
||||
v["why"] = json!(self.why);
|
||||
}
|
||||
if !self.preset.is_empty() {
|
||||
v["preset"] = json!(self.preset);
|
||||
}
|
||||
if self.active {
|
||||
v["active"] = json!(true);
|
||||
}
|
||||
if let Some(d) = &self.details {
|
||||
v["details"] = d.clone();
|
||||
}
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
/// `%.0f` du Go (arrondi au plus proche, demi vers le pair).
|
||||
pub fn f0(x: f64) -> String {
|
||||
format!("{x:.0}")
|
||||
}
|
||||
|
||||
/// `strataFitFor` : ce qu'un quant demande sur cette machine, et s'il y tient.
|
||||
pub fn fit_for(q: Quant, env: &Env, have: f64) -> Fit {
|
||||
let mut f = Fit {
|
||||
quant: q,
|
||||
disk_gb: (q.download_gb - have).max(10.0),
|
||||
mmap: env.avail_gb < q.arena_gb + LOW_RAM_HEADROOM_GB,
|
||||
drop: false,
|
||||
ok: false,
|
||||
why: String::new(),
|
||||
preset: String::new(),
|
||||
active: false,
|
||||
details: None,
|
||||
};
|
||||
let vram: f64 = env
|
||||
.gpus
|
||||
.iter()
|
||||
.filter(|g| g.index == env.main || g.index == env.helper)
|
||||
.map(|g| g.vram_gb)
|
||||
.sum();
|
||||
if env.ram_gb + vram - 12.0 < q.arena_gb {
|
||||
f.why = format!(
|
||||
"demande plus de mémoire : {} Go de RAM + VRAM, il en faut ~{}",
|
||||
f0(env.ram_gb + vram),
|
||||
f0(q.arena_gb + 12.0)
|
||||
);
|
||||
} else if env.disk_free_gb > 0.0 && env.disk_free_gb < f.disk_gb + 5.0 {
|
||||
f.why = format!(
|
||||
"pas assez d'espace disque : {} Go libres, il en faut ~{}",
|
||||
f0(env.disk_free_gb),
|
||||
f0(f.disk_gb + 5.0)
|
||||
);
|
||||
} else {
|
||||
f.ok = true;
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
/// `strataRecommend` : le quant le plus fidèle qui tient.
|
||||
pub fn recommend(paths: &Paths, family: &str, env: &Env) -> String {
|
||||
let Some(fam) = FAMILIES.iter().find(|f| f.id == family) else {
|
||||
return String::new();
|
||||
};
|
||||
let mut best = String::new();
|
||||
for id in ["IQ3_XXS", "IQ2_XS", "Q2_0"] {
|
||||
if !fam.quants.contains(&id) {
|
||||
continue;
|
||||
}
|
||||
let q = quant(id).expect("quant");
|
||||
if fit_for(q, env, have_gb(paths, family, id)).ok {
|
||||
best = id.to_string();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if fam.quants.contains(&"IQ3_S") {
|
||||
let q = quant("IQ3_S").expect("quant");
|
||||
let f = fit_for(q, env, have_gb(paths, family, "IQ3_S"));
|
||||
if f.ok && !f.mmap {
|
||||
best = "IQ3_S".into();
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
/// `strataPresetName`.
|
||||
pub fn preset_name(family: &str, quant: &str) -> String {
|
||||
let label = if family == "swift" {
|
||||
"Swift 1.5"
|
||||
} else {
|
||||
"Classique"
|
||||
};
|
||||
format!("STRATA FLASH NEXT {} {quant}", label.to_uppercase())
|
||||
}
|
||||
|
||||
/// `strataFamilyLabel`.
|
||||
pub fn family_label(id: &str) -> String {
|
||||
match FAMILIES.iter().find(|f| f.id == id) {
|
||||
Some(f) if f.id == "qwen" => "Qwen3.8 Flash Next".into(),
|
||||
Some(f) => format!("Qwen3.8 Flash Next {}", f.label),
|
||||
None => id.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `strataBool`.
|
||||
pub fn sbool(b: bool) -> &'static str {
|
||||
if b { "1" } else { "0" }
|
||||
}
|
||||
|
||||
/// `strataSettingsReq`.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SettingsReq {
|
||||
pub preset: String,
|
||||
pub ctx: i64,
|
||||
pub kv: String,
|
||||
pub spec: i64,
|
||||
pub vision: bool,
|
||||
pub helper: bool,
|
||||
pub experts: String,
|
||||
}
|
||||
|
||||
/// `strataApplySettings` : réécrit les clés du preset. Pure.
|
||||
pub fn apply_settings(content: &str, r: &SettingsReq) -> Result<String, String> {
|
||||
if r.ctx < 32768 || r.ctx > MAX_CTX || r.ctx % 4096 != 0 {
|
||||
return Err(format!("contexte hors limites : {}", r.ctx));
|
||||
}
|
||||
if r.kv != "fp16" && r.kv != "int8" {
|
||||
return Err(format!("cache KV inconnu : {}", r.kv));
|
||||
}
|
||||
if r.spec < 2 || r.spec > 4 {
|
||||
return Err(format!("MTP hors limites : {}", r.spec));
|
||||
}
|
||||
if !r.experts.is_empty()
|
||||
&& r.experts != EXPERTS_AUTO
|
||||
&& r.experts != EXPERTS_RAM
|
||||
&& r.experts != EXPERTS_DISK
|
||||
{
|
||||
return Err(format!("placement des experts inconnu : {}", r.experts));
|
||||
}
|
||||
let mut set: Vec<(&str, String)> = vec![
|
||||
("CTX", r.ctx.to_string()),
|
||||
("STRATA_KV", r.kv.clone()),
|
||||
("STRATA_SPEC", r.spec.to_string()),
|
||||
("STRATA_VISION", sbool(r.vision).into()),
|
||||
("STRATA_HELPER", sbool(r.helper).into()),
|
||||
];
|
||||
if !r.experts.is_empty() {
|
||||
set.push(("STRATA_EXPERTS", r.experts.clone()));
|
||||
}
|
||||
let mut lines: Vec<String> = Vec::new();
|
||||
for l in content.trim_end_matches('\n').split('\n') {
|
||||
let k = l.trim().split_once('=').map_or(l.trim(), |(k, _)| k);
|
||||
if set.iter().any(|(s, _)| *s == k) {
|
||||
continue;
|
||||
}
|
||||
lines.push(l.to_string());
|
||||
}
|
||||
for (k, v) in set {
|
||||
lines.push(format!("{k}={v}"));
|
||||
}
|
||||
Ok(lines.join("\n") + "\n")
|
||||
}
|
||||
|
||||
/// `strataHelperVRAM` : VRAM de la carte d'appoint du preset, 0 sans.
|
||||
pub fn helper_vram(cfg: &Cfg, gpus: &[Gpu]) -> f64 {
|
||||
if !helper_on(cfg) {
|
||||
return 0.0;
|
||||
}
|
||||
let h = cfg_get(cfg, "STRATA_HELPER_GPU").trim();
|
||||
gpus.iter()
|
||||
.find(|g| g.index.to_string() == h)
|
||||
.map_or(0.0, |g| g.vram_gb)
|
||||
}
|
||||
|
||||
/// `strataFindConfig` : la config que l'installeur a écrite pour ce quant.
|
||||
pub fn find_config(src_dir: &Path, quant: &str) -> Result<String, String> {
|
||||
let q = quant.to_lowercase();
|
||||
let mut names: Vec<String> = std::fs::read_dir(src_dir)
|
||||
.map(|rd| {
|
||||
rd.flatten()
|
||||
.map(|e| e.file_name().to_string_lossy().into_owned())
|
||||
.filter(|n| n.starts_with("strata-") && n.ends_with(".json"))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
names.sort();
|
||||
for n in names {
|
||||
if n.to_lowercase().contains(&q) {
|
||||
return Ok(src_dir.join(n).to_string_lossy().into_owned());
|
||||
}
|
||||
}
|
||||
Err(format!(
|
||||
"l'installeur n'a pas laissé de configuration pour {quant} dans {}",
|
||||
src_dir.to_string_lossy()
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
Reference in new issue
Block a user