use std::sync::Arc;
use mold_core::{
build_model_catalog, Config, GenerateRequest, ModelDefaults, ModelInfo, ModelInfoExtended,
ModelPaths,
};
use mold_inference::device::{activation_bytes, activation_family_for, ActivationFamily};
use crate::model_cache::ModelResidency;
use crate::{routes::ApiError, state::AppState};
pub(crate) type EngineProgressCallback = Arc<dyn Fn(mold_inference::ProgressEvent) + Send + Sync>;
fn transformer_path_lower(paths: &ModelPaths) -> String {
paths.transformer.to_string_lossy().to_ascii_lowercase()
}
fn transformer_path_looks_flux2(path: &str) -> bool {
path.contains("/flux2/") || path.contains("flux2")
}
fn transformer_path_looks_ltx2(path: &str) -> bool {
path.contains("/ltx2/") || path.contains("ltx2")
}
fn transformer_path_looks_zimage(path: &str) -> bool {
path.contains("/z-image/") || path.contains("zimage")
}
#[derive(Debug, Clone, Copy)]
pub struct ActivationHint {
pub width: u32,
pub height: u32,
pub batch: u32,
pub dtype_bytes: u32,
pub family: ActivationFamily,
}
impl ActivationHint {
pub fn from_request(req: &GenerateRequest, family_slug: &str) -> Self {
let family = activation_family_for(family_slug);
let batch = match family {
ActivationFamily::SdxlUnet | ActivationFamily::Sd3Mmdit if req.guidance > 1.0 => 2,
_ => 1,
};
Self {
width: req.width,
height: req.height,
batch,
dtype_bytes: 2,
family,
}
}
pub fn budget_bytes(&self) -> u64 {
activation_bytes(
self.width,
self.height,
self.batch,
self.dtype_bytes,
self.family,
)
}
}
fn check_model_memory_budget(
model_name: &str,
peak_bytes: u64,
available_bytes: u64,
suggestion: &str,
) -> Result<(), ApiError> {
let hard_limit = available_bytes * 9 / 10; if peak_bytes > hard_limit {
return Err(ApiError::insufficient_memory(format!(
"model '{}' estimated peak ~{:.1} GB exceeds the per-load budget cap ~{:.1} GB \
(90% of {:.1} GB free, with 2 GB activation headroom built into peak estimate; \
encoders are dropped before denoise). {}",
model_name,
peak_bytes as f64 / 1_000_000_000.0,
hard_limit as f64 / 1_000_000_000.0,
available_bytes as f64 / 1_000_000_000.0,
suggestion,
)));
}
let warn_limit = available_bytes * 8 / 10; if peak_bytes > warn_limit {
tracing::warn!(
model = %model_name,
peak_gb = format_args!("{:.1}", peak_bytes as f64 / 1_000_000_000.0),
available_gb = format_args!("{:.1}", available_bytes as f64 / 1_000_000_000.0),
"model is close to memory limit — may trigger page reclamation"
);
}
Ok(())
}
fn rejection_suggestion(hint: Option<ActivationHint>) -> &'static str {
match hint.map(|h| h.family) {
Some(ActivationFamily::LtxVideo) => {
"Try reducing --frames or --width/--height, use a quantized variant \
(e.g. ':q8'), or close other GPU apps."
}
_ => {
"Try lowering --width/--height, reduce --batch, use a smaller/quantized \
variant if available, enable --offload for FLUX, or close other GPU apps."
}
}
}
pub(crate) fn preflight_memory_guard_with_available(
model_name: &str,
paths: &ModelPaths,
active_vram_bytes: u64,
available_bytes: u64,
hint: Option<ActivationHint>,
) -> Result<(), ApiError> {
let transformer_path = transformer_path_lower(paths);
let streaming = hint
.map(|h| h.family.streaming_transformer())
.unwrap_or_else(|| transformer_path_looks_ltx2(&transformer_path));
let forced_flux_offload = hint.is_some_and(|h| h.family == ActivationFamily::FluxDit)
&& std::env::var("MOLD_OFFLOAD").is_ok_and(|v| v == "1");
let qwen_quantized = hint.is_some_and(|h| h.family == ActivationFamily::QwenImageDit)
&& paths
.transformer
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| e.eq_ignore_ascii_case("gguf"));
let peak = if streaming {
let gemma_competes = ltx2_encoder_phase_competes_with_transformer_gpu(0);
streaming_transformer_peak(paths, gemma_competes)
} else if forced_flux_offload {
streaming_transformer_peak(paths, false)
} else if hint.is_some_and(|h| h.family == ActivationFamily::Sd3Mmdit) {
sd3_sequential_peak(paths)
} else if qwen_quantized {
qwen_image_quantized_sequential_peak(paths, hint)
} else {
mold_inference::device::estimate_peak_memory(
paths,
mold_inference::LoadStrategy::Sequential,
)
};
let activation = if qwen_quantized {
0
} else {
hint.map(|h| h.budget_bytes()).unwrap_or(0)
};
let peak_with_activation = peak.saturating_add(activation);
let effective_available = available_bytes.saturating_add(active_vram_bytes);
if qwen_quantized && peak_with_activation <= effective_available {
return Ok(());
}
let suggestion = rejection_suggestion(hint);
check_model_memory_budget(
model_name,
peak_with_activation,
effective_available,
suggestion,
)
}
fn streaming_transformer_peak(
paths: &ModelPaths,
gemma_competes_with_transformer_gpu: bool,
) -> u64 {
const STREAMING_TRANSFORMER_CAP: u64 = 6_000_000_000; const HEADROOM: u64 = 2_000_000_000;
let file_size = |p: &std::path::Path| std::fs::metadata(p).map(|m| m.len()).unwrap_or(0);
let t5_size = paths.t5_encoder.as_ref().map(|p| file_size(p)).unwrap_or(0);
let clip_size = paths
.clip_encoder
.as_ref()
.map(|p| file_size(p))
.unwrap_or(0);
let clip2_size = paths
.clip_encoder_2
.as_ref()
.map(|p| file_size(p))
.unwrap_or(0);
let text_encoder_size: u64 = paths.text_encoder_files.iter().map(|p| file_size(p)).sum();
let encoder_total = if gemma_competes_with_transformer_gpu {
t5_size + clip_size + clip2_size + text_encoder_size
} else {
0
};
let inference_phase = STREAMING_TRANSFORMER_CAP;
std::cmp::max(encoder_total, inference_phase) + HEADROOM
}
fn sd3_sequential_peak(paths: &ModelPaths) -> u64 {
const SD3_VAE_RESIDENCY_CAP: u64 = 1_000_000_000; const HEADROOM: u64 = 2_000_000_000;
let file_size = |p: &std::path::Path| std::fs::metadata(p).map(|m| m.len()).unwrap_or(0);
let transformer_size = if !paths.transformer_shards.is_empty() {
paths.transformer_shards.iter().map(|p| file_size(p)).sum()
} else {
file_size(&paths.transformer)
};
let vae_size = file_size(&paths.vae).min(SD3_VAE_RESIDENCY_CAP);
let t5_size = paths.t5_encoder.as_ref().map(|p| file_size(p)).unwrap_or(0);
let clip_size = paths
.clip_encoder
.as_ref()
.map(|p| file_size(p))
.unwrap_or(0);
let clip2_size = paths
.clip_encoder_2
.as_ref()
.map(|p| file_size(p))
.unwrap_or(0);
let text_encoder_size: u64 = paths.text_encoder_files.iter().map(|p| file_size(p)).sum();
let encoder_total = t5_size + clip_size + clip2_size + text_encoder_size;
transformer_size.max(vae_size).max(encoder_total) + HEADROOM
}
fn qwen_image_quantized_sequential_peak(paths: &ModelPaths, hint: Option<ActivationHint>) -> u64 {
const QWEN_GGUF_PHASE_HEADROOM: u64 = 128_000_000;
let file_size = |p: &std::path::Path| std::fs::metadata(p).map(|m| m.len()).unwrap_or(0);
let transformer_size = if !paths.transformer_shards.is_empty() {
paths.transformer_shards.iter().map(|p| file_size(p)).sum()
} else {
file_size(&paths.transformer)
};
let text_encoder_size: u64 = paths.text_encoder_files.iter().map(|p| file_size(p)).sum();
let vae_size = file_size(&paths.vae);
let activation = hint
.map(|h| {
mold_inference::device::activation_bytes(
h.width,
h.height,
1,
h.dtype_bytes,
ActivationFamily::QwenImageDit,
)
})
.unwrap_or(0);
transformer_size
.saturating_add(activation)
.saturating_add(QWEN_GGUF_PHASE_HEADROOM)
.max(text_encoder_size)
.max(vae_size)
}
fn ltx2_encoder_phase_competes_with_transformer_gpu(gpu_ordinal: usize) -> bool {
matches!(
mold_inference::device::resolve_ltx2_gemma_device_override(gpu_ordinal),
Some(mold_inference::device::LtxGemmaPlacement::Gpu(ordinal)) if ordinal == gpu_ordinal
)
}
pub(crate) fn preflight_memory_guard(
model_name: &str,
paths: &ModelPaths,
active_vram_bytes: u64,
#[cfg_attr(not(feature = "cuda"), allow(unused_variables))] gpu_ordinal: usize,
hint: Option<ActivationHint>,
) -> Result<(), ApiError> {
#[cfg(feature = "cuda")]
{
if active_vram_bytes > 0 {
if let Some(total) = mold_inference::device::total_vram_bytes(gpu_ordinal) {
return preflight_memory_guard_with_available(model_name, paths, 0, total, hint);
}
}
if let (Some(free), Some(total)) = (
mold_inference::device::free_vram_bytes(gpu_ordinal),
mold_inference::device::total_vram_bytes(gpu_ordinal),
) {
const GHOST_VRAM_THRESHOLD: u64 = 1_500_000_000; if total.saturating_sub(free) > GHOST_VRAM_THRESHOLD {
tracing::info!(
gpu = gpu_ordinal,
free_gb = format_args!("{:.1}", free as f64 / 1e9),
total_gb = format_args!("{:.1}", total as f64 / 1e9),
"no active model on this GPU but VRAM is held — reclaiming primary context",
);
mold_inference::device::reclaim_gpu_memory(gpu_ordinal);
}
let effective_free = mold_inference::device::usable_free_vram_bytes(gpu_ordinal)
.unwrap_or_else(|| {
free.saturating_sub(mold_inference::device::reserved_vram_bytes())
});
return preflight_memory_guard_with_available(
model_name,
paths,
active_vram_bytes,
effective_free,
hint,
);
}
if let Some(free) = mold_inference::device::usable_free_vram_bytes(gpu_ordinal) {
return preflight_memory_guard_with_available(
model_name,
paths,
active_vram_bytes,
free,
hint,
);
}
}
if let Some(available) = mold_inference::device::available_system_memory_bytes() {
if available > 0 {
return preflight_memory_guard_with_available(
model_name,
paths,
active_vram_bytes,
available,
hint,
);
}
}
Ok(())
}
pub(crate) fn effective_load_available_bytes(
active_vram_bytes: u64,
#[cfg_attr(not(feature = "cuda"), allow(unused_variables))] gpu_ordinal: usize,
) -> Option<u64> {
#[cfg(feature = "cuda")]
{
if active_vram_bytes > 0 {
if let Some(total) = mold_inference::device::total_vram_bytes(gpu_ordinal) {
return Some(total);
}
}
if let Some(free) = mold_inference::device::usable_free_vram_bytes(gpu_ordinal) {
return Some(free);
}
}
mold_inference::device::available_system_memory_bytes()
.filter(|available| *available > 0)
.map(|available| available.saturating_add(active_vram_bytes))
}
pub(crate) fn select_server_load_strategy_for_budget(
paths: &ModelPaths,
available_bytes: Option<u64>,
hint: Option<ActivationHint>,
) -> mold_inference::LoadStrategy {
let transformer_is_gguf = paths
.transformer
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| e.eq_ignore_ascii_case("gguf"));
if hint.is_some_and(|h| h.family == ActivationFamily::ZImageDit) && !transformer_is_gguf {
return mold_inference::LoadStrategy::Sequential;
}
if transformer_is_gguf
&& hint.is_some_and(|h| {
matches!(
h.family,
ActivationFamily::Sd3Mmdit | ActivationFamily::ZImageDit
)
})
{
return mold_inference::LoadStrategy::Eager;
}
let qwen_quantized =
hint.is_some_and(|h| h.family == ActivationFamily::QwenImageDit) && transformer_is_gguf;
let Some(available_bytes) = available_bytes.filter(|v| *v > 0) else {
return mold_inference::LoadStrategy::Eager;
};
if qwen_quantized {
let peak = qwen_image_quantized_sequential_peak(paths, hint);
if peak <= available_bytes {
return mold_inference::LoadStrategy::Sequential;
}
}
let activation = hint.map(|h| h.budget_bytes()).unwrap_or(0);
let eager_peak =
mold_inference::device::estimate_peak_memory(paths, mold_inference::LoadStrategy::Eager)
.saturating_add(activation);
let sequential_peak = mold_inference::device::estimate_peak_memory(
paths,
mold_inference::LoadStrategy::Sequential,
)
.saturating_add(activation);
let hard_limit = available_bytes.saturating_mul(9) / 10;
if eager_peak > hard_limit && sequential_peak <= hard_limit {
mold_inference::LoadStrategy::Sequential
} else {
mold_inference::LoadStrategy::Eager
}
}
pub(crate) fn select_server_load_strategy_for_device(
paths: &ModelPaths,
available_bytes: Option<u64>,
device_total_bytes: Option<u64>,
hint: Option<ActivationHint>,
) -> mold_inference::LoadStrategy {
let capped_available = match (
available_bytes.filter(|available| *available > 0),
device_total_bytes.filter(|total| *total > 0),
) {
(Some(available), Some(total)) => Some(available.min(total)),
(available, None) => available,
(None, Some(total)) => Some(total),
};
select_server_load_strategy_for_budget(paths, capped_available, hint)
}
pub(crate) fn server_offload_enabled_for_paths(
paths: &ModelPaths,
hint: Option<ActivationHint>,
request_has_lora: bool,
) -> bool {
if !std::env::var("MOLD_OFFLOAD").is_ok_and(|v| v == "1") {
return false;
}
let transformer_path = transformer_path_lower(paths);
let transformer_looks_flux2 = transformer_path_looks_flux2(&transformer_path);
let transformer_looks_zimage = transformer_path_looks_zimage(&transformer_path);
let transformer_looks_nvfp4 = transformer_path.contains("nvfp4");
if request_has_lora
&& (transformer_looks_flux2
|| transformer_looks_zimage
|| hint.is_some_and(|h| {
matches!(
h.family,
ActivationFamily::Flux2Dit | ActivationFamily::ZImageDit
)
}))
{
return false;
}
let transformer_is_gguf = paths
.transformer
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| e.eq_ignore_ascii_case("gguf"));
if transformer_looks_nvfp4
&& (transformer_looks_flux2 || hint.is_some_and(|h| h.family == ActivationFamily::Flux2Dit))
{
return false;
}
!(transformer_is_gguf
&& hint.is_some_and(|h| {
matches!(
h.family,
ActivationFamily::Sd3Mmdit
| ActivationFamily::ZImageDit
| ActivationFamily::Flux2Dit
)
}))
}
pub(crate) fn request_requires_fresh_engine_for_offload_policy(
paths: &ModelPaths,
hint: Option<ActivationHint>,
request_has_lora: bool,
) -> bool {
request_has_lora
&& server_offload_enabled_for_paths(paths, hint, false)
&& !server_offload_enabled_for_paths(paths, hint, true)
}
pub(crate) fn request_has_effective_lora(req: &GenerateRequest) -> bool {
const ZERO_SCALE_EPS: f64 = 1e-8;
if let Some(loras) = &req.loras {
if !loras.is_empty() {
return loras.iter().any(|lora| lora.scale.abs() > ZERO_SCALE_EPS);
}
}
req.lora
.as_ref()
.is_some_and(|lora| lora.scale.abs() > ZERO_SCALE_EPS)
}
pub(crate) fn resolve_installed_catalog_paths_for_worker(
model_name: &str,
config: &Config,
) -> Result<Option<(ModelPaths, Config)>, ApiError> {
if !looks_like_catalog_id(model_name) {
return Ok(None);
}
let Some(intent) =
installed_catalog_intent_from_sidecar(&config.resolved_models_dir(), model_name)
else {
return Ok(None);
};
let model_cfg = resolve_intent_to_paths(model_name, &intent, config)
.map_err(|e| resolve_error_to_api_error(&e))?;
let mut resolved_config = config.clone();
resolved_config
.models
.insert(model_name.to_string(), model_cfg);
let paths = ModelPaths::resolve(model_name, &resolved_config).ok_or_else(|| {
ApiError::not_found(format!(
"catalog model '{model_name}' resolved to a config that ModelPaths \
could not turn into runtime paths — internal mismatch, please file an issue."
))
})?;
Ok(Some((paths, resolved_config)))
}
pub(crate) type DownloadProgressCallback =
Arc<dyn Fn(mold_core::download::DownloadProgressEvent) + Send + Sync>;
pub(crate) enum PullStatus {
AlreadyAvailable,
Pulled,
}
pub(crate) async fn refresh_config(state: &AppState) -> mold_core::Config {
let fresh = {
let current = state.config.read().await;
current.reload_from_disk_preserving_runtime()
};
let mut config = state.config.write().await;
*config = fresh.clone();
fresh
}
pub(crate) async fn list_models(state: &AppState) -> Vec<ModelInfoExtended> {
let config = refresh_config(state).await;
let models_dir = config.resolved_models_dir();
if state.gpu_pool.worker_count() > 0 {
let loaded_models = loaded_models_across_pool(state);
let primary = loaded_models.first().cloned();
let mut catalog = build_model_catalog(&config, primary.as_deref(), primary.is_some());
for entry in catalog.iter_mut() {
if loaded_models.contains(&entry.info.name) {
entry.info.is_loaded = true;
}
}
catalog.extend(installed_catalog_models(
state,
&config,
&models_dir,
primary.as_deref(),
primary.is_some(),
));
return catalog;
}
let snapshot = state.model_cache.lock().await.snapshot();
let mut catalog =
build_model_catalog(&config, snapshot.model_name.as_deref(), snapshot.is_loaded);
catalog.extend(installed_catalog_models(
state,
&config,
&models_dir,
snapshot.model_name.as_deref(),
snapshot.is_loaded,
));
catalog
}
fn loaded_models_across_pool(state: &AppState) -> Vec<String> {
let mut names = Vec::new();
for worker in &state.gpu_pool.workers {
let active = worker
.active_generation
.read()
.ok()
.and_then(|g| g.as_ref().map(|g| g.model.clone()));
let loaded = active.or_else(|| {
let cache = worker.model_cache.lock().ok()?;
cache.active_model().map(|s| s.to_string())
});
if let Some(name) = loaded {
if !names.contains(&name) {
names.push(name);
}
}
}
names
}
fn looks_like_catalog_id(id: &str) -> bool {
id.starts_with("cv:") || id.starts_with("hf:")
}
pub(crate) async fn catalog_family_for(state: &AppState, model_name: &str) -> Option<String> {
if !looks_like_catalog_id(model_name) {
return None;
}
{
let intents = state.catalog_intents.read().await;
if let Some(intent) = intents.get(model_name) {
return Some(intent.family.clone());
}
}
let config = state.config.read().await;
if let Some(family) = config.models.get(model_name).and_then(|m| m.family.clone()) {
return Some(family);
}
installed_catalog_intent_from_sidecar(&config.resolved_models_dir(), model_name)
.map(|intent| intent.family)
}
pub(crate) async fn family_for_model(state: &AppState, model_name: &str) -> Option<String> {
if let Some(manifest) = mold_core::manifest::find_manifest(model_name) {
return Some(manifest.family.clone());
}
catalog_family_for(state, model_name).await
}
pub(crate) fn family_for_model_sync(
model_name: &str,
config: &mold_core::Config,
) -> Option<String> {
if let Some(manifest) = mold_core::manifest::find_manifest(model_name) {
return Some(manifest.family.clone());
}
config.models.get(model_name).and_then(|m| m.family.clone())
}
pub(crate) fn activation_hint_for_request_sync(
config: &mold_core::Config,
req: &GenerateRequest,
) -> Option<ActivationHint> {
let family = family_for_model_sync(&req.model, config)?;
Some(ActivationHint::from_request(req, &family))
}
pub(crate) async fn activation_hint_for_request(
state: &AppState,
req: &GenerateRequest,
) -> Option<ActivationHint> {
let family = family_for_model(state, &req.model).await?;
Some(ActivationHint::from_request(req, &family))
}
fn copy_catalog_companion(cfg: &mut mold_core::ModelConfig, companion: &str, paths: &ModelPaths) {
let to_str = |p: &std::path::PathBuf| p.to_str().map(str::to_owned);
match companion {
"clip-l" => {
cfg.clip_encoder = to_str(&paths.transformer);
cfg.clip_tokenizer = paths.clip_tokenizer.as_ref().and_then(to_str);
}
"clip-g" => {
cfg.clip_encoder_2 = to_str(&paths.transformer);
cfg.clip_tokenizer_2 = paths
.clip_tokenizer
.as_ref()
.and_then(to_str)
.or_else(|| cfg.clip_tokenizer.clone());
}
"sdxl-vae" | "sd-vae-ft-mse" => {}
"flux-vae" if cfg.vae.is_none() => {
cfg.vae = to_str(&paths.transformer);
}
"ltx-video-vae" | "flux2-vae" => {
cfg.vae = to_str(&paths.transformer);
}
"t5-v1_1-xxl" => {
cfg.t5_encoder = to_str(&paths.transformer);
cfg.t5_tokenizer = paths.t5_tokenizer.as_ref().and_then(to_str);
}
"z-image-te" | "flux2-te" | "flux2-te-9b" => {
if companion == "z-image-te" && cfg.vae.is_none() {
cfg.vae = to_str(&paths.vae);
}
if companion != "z-image-te" || cfg.text_encoder_files.is_none() {
cfg.text_encoder_files = paths
.text_encoder_files
.iter()
.filter_map(to_str)
.collect::<Vec<_>>()
.into();
}
cfg.text_tokenizer = paths.text_tokenizer.as_ref().and_then(to_str);
}
"ltx2-te" => {
cfg.text_encoder_files = paths
.text_encoder_files
.iter()
.filter_map(to_str)
.collect::<Vec<_>>()
.into();
}
_ => {}
}
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum ResolveError {
#[error(
"catalog model '{model}' is missing required component '{companion}'.\n\
The manifest entry for '{companion}' is not present in your config — \
install the matching base model first."
)]
CompanionConfigMissing {
model: String,
companion: &'static str,
},
#[error(
"catalog model '{model}' is missing required component '{companion}'.\n\
The companion file isn't on disk yet. Run `mold pull {model}` to fetch \
the primary checkpoint AND its companion files."
)]
CompanionFileMissing {
model: String,
companion: &'static str,
},
#[error(
"catalog model '{model}' is missing its primary checkpoint.\n\
Run `mold pull {model}` to fetch or repair the primary checkpoint."
)]
PrimaryFileMissing { model: String },
#[error("probe of FLUX checkpoint {path} for bundled VAE failed: {source}")]
FluxProbeFailed {
path: String,
#[source]
source: anyhow::Error,
},
}
fn known_companion_static(name: &str) -> Option<&'static str> {
Some(match name {
"clip-l" => "clip-l",
"clip-g" => "clip-g",
"sdxl-vae" => "sdxl-vae",
"sd-vae-ft-mse" => "sd-vae-ft-mse",
"flux-vae" => "flux-vae",
"flux2-te" => "flux2-te",
"flux2-te-9b" => "flux2-te-9b",
"flux2-vae" => "flux2-vae",
"z-image-te" => "z-image-te",
"ltx-video-vae" => "ltx-video-vae",
"ltx2-te" => "ltx2-te",
"t5-v1_1-xxl" => "t5-v1_1-xxl",
_ => return None,
})
}
pub(crate) fn resolve_intent_to_paths(
model_name: &str,
intent: &mold_catalog::synthesis::CatalogModelIntent,
config: &mold_core::Config,
) -> Result<mold_core::ModelConfig, ResolveError> {
if !catalog_primary_is_complete(model_name, intent, config) {
return Err(ResolveError::PrimaryFileMissing {
model: model_name.to_string(),
});
}
let primary_str = intent
.primary_recipe_path
.to_str()
.expect("synthesize_intent guarantees UTF-8 paths")
.to_string();
let mut cfg = mold_core::ModelConfig {
family: Some(intent.family.clone()),
..Default::default()
};
apply_catalog_runtime_defaults(&mut cfg, intent);
cfg.transformer = Some(primary_str.clone());
let is_flux = intent.family == "flux";
let probe_says_bundled = if is_flux && intent.primary_recipe_path.exists() {
Some(
mold_inference::loader::flux_single_file_bundles_vae(&intent.primary_recipe_path)
.map_err(|e| ResolveError::FluxProbeFailed {
path: intent.primary_recipe_path.display().to_string(),
source: e.into(),
})?,
)
} else if is_flux {
None
} else if mold_catalog::synthesis::family_bundles_vae_unconditionally(flux_family_for_slug(
&intent.family,
)) {
Some(true)
} else {
Some(false)
};
if probe_says_bundled == Some(true) {
cfg.vae = Some(primary_str);
}
if let Some(vae_recipe_path) = &intent.vae_recipe_path {
cfg.vae = Some(
vae_recipe_path
.to_str()
.expect("synthesize_intent guarantees UTF-8 paths")
.to_string(),
);
}
if !intent.text_encoder_recipe_paths.is_empty() {
cfg.text_encoder_files = Some(
intent
.text_encoder_recipe_paths
.iter()
.map(|path| {
path.to_str()
.expect("synthesize_intent guarantees UTF-8 paths")
.to_string()
})
.collect(),
);
}
let mut missing_required: Option<&'static str> = None;
for companion in &intent.companions {
let static_name =
known_companion_static(&companion.name).expect("companion name in registry");
match ModelPaths::resolve(static_name, config) {
Some(paths) => copy_catalog_companion(&mut cfg, static_name, &paths),
None => {
if companion.required {
if missing_required.is_none() {
missing_required = Some(static_name);
}
}
}
}
}
if let Some(missing) = missing_required {
return Err(classify_missing_companion(model_name, missing, config));
}
Ok(cfg)
}
fn apply_catalog_runtime_defaults(
cfg: &mut mold_core::ModelConfig,
intent: &mold_catalog::synthesis::CatalogModelIntent,
) {
if intent.family == "flux" {
match intent.sub_family.as_deref() {
Some("flux1-s") => {
cfg.is_schnell.get_or_insert(true);
cfg.default_steps.get_or_insert(4);
cfg.default_guidance.get_or_insert(0.0);
}
Some("flux1-d" | "flux1-krea" | "flux1-kontext") => {
cfg.is_schnell.get_or_insert(false);
}
_ => {}
}
}
}
fn catalog_primary_is_complete(
model_name: &str,
intent: &mold_catalog::synthesis::CatalogModelIntent,
config: &mold_core::Config,
) -> bool {
if !intent.primary_recipe_path.is_file() {
return false;
}
if model_name.starts_with("cv:") {
let models_dir = config.resolved_models_dir();
let sc_path = mold_catalog::sidecar::civitai_sidecar_path(&models_dir, model_name);
if let Ok(sidecar) = mold_catalog::sidecar::read_sidecar(&sc_path) {
let Some(sidecar_dir) = sc_path.parent() else {
return false;
};
return mold_catalog::sidecar::primary_path_if_present(sidecar_dir, &sidecar).is_some();
}
}
true
}
fn flux_family_for_slug(slug: &str) -> mold_catalog::families::Family {
use mold_catalog::families::Family;
match slug {
"flux" => Family::Flux,
"flux2" => Family::Flux2,
"sd15" => Family::Sd15,
"sdxl" => Family::Sdxl,
"z-image" => Family::ZImage,
"ltx-video" => Family::LtxVideo,
"ltx2" => Family::Ltx2,
"qwen-image" => Family::QwenImage,
"wuerstchen" => Family::Wuerstchen,
_ => Family::Flux,
}
}
fn classify_missing_companion(
model_name: &str,
companion: &'static str,
config: &mold_core::Config,
) -> ResolveError {
if !config.models.contains_key(companion) {
ResolveError::CompanionConfigMissing {
model: model_name.to_string(),
companion,
}
} else {
ResolveError::CompanionFileMissing {
model: model_name.to_string(),
companion,
}
}
}
#[cfg(test)]
fn synthesize_catalog_config(
entry: &mold_catalog::entry::CatalogEntry,
models_dir: &std::path::Path,
config: &mold_core::Config,
) -> anyhow::Result<mold_core::ModelConfig> {
let intent = mold_catalog::synthesis::synthesize_intent(entry, models_dir)?;
Ok(resolve_intent_to_paths(entry.id.as_str(), &intent, config)?)
}
pub(crate) async fn install_catalog_model(
state: &AppState,
model_name: &str,
) -> Result<(), mold_core::InstallError> {
if !looks_like_catalog_id(model_name) {
return Ok(());
}
{
let intents = state.catalog_intents.read().await;
if intents.contains_key(model_name) {
return Ok(());
}
}
let models_dir = state.config.read().await.resolved_models_dir();
if let Some(intent) = installed_catalog_intent_from_sidecar(&models_dir, model_name) {
let mut intents = state.catalog_intents.write().await;
intents.insert(model_name.to_string(), intent);
return Ok(());
}
let civitai_base = state.catalog_live_civitai_base.as_str();
let entry = if let Some(version_id) = model_name.strip_prefix("cv:") {
mold_catalog::live::fetch_civitai_version(
civitai_base,
version_id,
std::env::var("CIVITAI_TOKEN").ok().as_deref(),
)
.await
.map_err(|e| live_error_to_install_error(model_name, &e))?
} else if let Some(repo_id) = model_name.strip_prefix("hf:") {
mold_catalog::live::fetch_hf_repo(
"https://huggingface.co",
repo_id,
std::env::var("HF_TOKEN").ok().as_deref(),
)
.await
.map_err(|e| live_error_to_install_error(model_name, &e))?
} else {
return Ok(());
};
let intent = mold_catalog::synthesis::synthesize_intent(&entry, &models_dir).map_err(|e| {
mold_core::InstallError::RecipeMalformed(format!("synthesize intent for {model_name}: {e}"))
})?;
if model_name.starts_with("cv:") {
write_catalog_sidecar_from_intent(&models_dir, &entry, &intent);
}
let mut intents = state.catalog_intents.write().await;
intents.insert(model_name.to_string(), intent);
Ok(())
}
fn write_catalog_sidecar_from_intent(
models_dir: &std::path::Path,
entry: &mold_catalog::entry::CatalogEntry,
intent: &mold_catalog::synthesis::CatalogModelIntent,
) {
let sc_path = mold_catalog::sidecar::civitai_sidecar_path(models_dir, entry.id.as_str());
let Some(sidecar_dir) = sc_path.parent() else {
return;
};
let Ok(primary_rel) = intent.primary_recipe_path.strip_prefix(sidecar_dir) else {
return;
};
let Some(primary_rel) = primary_rel.to_str() else {
return;
};
let sidecar = mold_catalog::sidecar::sidecar_from_entry(entry, primary_rel.to_string());
if let Err(e) = mold_catalog::sidecar::write_sidecar(&sc_path, &sidecar) {
tracing::warn!(
target: "catalog.sidecar",
catalog_id = %entry.id.as_str(),
error = %e,
"sidecar write failed after live catalog install",
);
}
}
fn installed_catalog_intent_from_sidecar(
models_dir: &std::path::Path,
model_name: &str,
) -> Option<mold_catalog::synthesis::CatalogModelIntent> {
let (sidecar_dir, sidecar) = mold_catalog::sidecar::walk_sidecars(models_dir)
.into_iter()
.find(|(_, sidecar)| sidecar.id == model_name)?;
if sidecar.kind != "checkpoint" {
return None;
}
if sidecar_primary_looks_like_auxiliary(&sidecar) {
return None;
}
let primary_recipe_path =
mold_catalog::sidecar::primary_path_if_present(&sidecar_dir, &sidecar)?;
let family = mold_catalog::families::Family::from_str(&sidecar.family).ok()?;
let companions = mold_catalog::companions::companions_for(
family,
sidecar.sub_family.as_deref(),
mold_catalog::entry::Bundling::SingleFile,
mold_catalog::entry::Kind::Checkpoint,
)
.into_iter()
.map(|name| mold_catalog::synthesis::CompanionIntent {
name,
required: true,
})
.collect();
Some(mold_catalog::synthesis::CatalogModelIntent {
family: sidecar.family,
sub_family: sidecar.sub_family,
primary_recipe_path,
vae_recipe_path: None,
text_encoder_recipe_paths: Vec::new(),
companions,
bundling: mold_catalog::entry::Bundling::SingleFile,
})
}
fn sidecar_primary_looks_like_auxiliary(sidecar: &mold_catalog::sidecar::CatalogSidecar) -> bool {
let rel = sidecar.primary_filename_rel.to_ascii_lowercase();
rel.contains("/text_encoder/")
|| rel.contains("text_encoder")
|| rel.contains("te_")
|| rel.contains("_txt.")
|| rel.contains("-txt.")
}
fn live_error_to_install_error(
model_name: &str,
err: &mold_catalog::live::LiveSearchError,
) -> mold_core::InstallError {
use mold_catalog::live::LiveSearchError;
match err {
LiveSearchError::Network(e) => {
mold_core::InstallError::Network(format!("{model_name}: {e}"))
}
LiveSearchError::Decode(e) => mold_core::InstallError::RecipeMalformed(format!(
"{model_name}: decode upstream payload: {e}"
)),
LiveSearchError::Upstream { status, body, .. } if *status == 404 => {
mold_core::InstallError::NotFound(format!(
"{model_name}: upstream returned 404 ({})",
truncate_body(body)
))
}
LiveSearchError::Upstream { status, body, .. } => {
mold_core::InstallError::RecipeMalformed(format!(
"{model_name}: upstream HTTP {status}: {}",
truncate_body(body)
))
}
}
}
fn truncate_body(body: &str) -> String {
let trimmed = body.trim();
if trimmed.len() > 160 {
format!("{}…", &trimmed[..160])
} else {
trimmed.to_string()
}
}
pub(crate) fn install_error_to_api_error(err: &mold_core::InstallError) -> ApiError {
use mold_core::InstallError;
match err {
InstallError::Network(msg) => {
ApiError::internal_with_status(
format!("network unreachable: {msg}"),
axum::http::StatusCode::BAD_GATEWAY,
)
}
InstallError::NotFound(msg) => ApiError::not_found(msg.to_string()),
InstallError::RecipeMalformed(msg) => ApiError::internal(msg.to_string()),
}
}
pub(crate) fn resolve_error_to_api_error(err: &ResolveError) -> ApiError {
ApiError::not_found(err.to_string())
}
fn installed_catalog_models(
_state: &AppState,
config: &mold_core::Config,
models_dir: &std::path::Path,
loaded_model: Option<&str>,
engine_is_loaded: bool,
) -> Vec<ModelInfoExtended> {
let walked = mold_catalog::sidecar::walk_sidecars(models_dir);
let mut out = Vec::new();
for (sidecar_dir, sidecar) in walked {
if sidecar.kind != "checkpoint" {
continue;
}
if sidecar_primary_looks_like_auxiliary(&sidecar) {
continue;
}
if mold_catalog::sidecar::primary_path_if_present(&sidecar_dir, &sidecar).is_none() {
continue;
}
if mold_core::manifest::find_manifest_by_hf_repo(&sidecar.source_id).is_some() {
continue;
}
let size_gb = sidecar
.size_bytes
.map(|b| b as f32 / 1_000_000_000.0)
.unwrap_or(0.0);
let (w, h, steps, guidance) = mold_core::manifest::visible_manifests()
.find(|m| m.family == sidecar.family)
.map(|m| {
let cfg = config.resolved_model_config(&m.name);
(
cfg.effective_width(config),
cfg.effective_height(config),
cfg.effective_steps(config),
cfg.effective_guidance(),
)
})
.unwrap_or_else(|| match sidecar.family.as_str() {
"ltx-video" | "ltx2" => (768, 512, 25, 3.5),
"sdxl" => (1024, 1024, 20, 7.5),
"sd15" => (512, 512, 20, 7.5),
"flux" => (1024, 1024, 20, 3.5),
"flux2" => (512, 512, 4, 0.0),
_ => (1024, 1024, 20, 3.5),
});
let description = match &sidecar.author {
Some(a) if !a.is_empty() => format!("{} by {a}", sidecar.name),
_ => sidecar.name.clone(),
};
out.push(ModelInfoExtended {
downloaded: true,
defaults: ModelDefaults {
default_width: w,
default_height: h,
default_steps: steps,
default_guidance: guidance,
description,
},
info: ModelInfo {
name: sidecar.id.clone(),
family: sidecar.family.clone(),
size_gb,
is_loaded: loaded_model.is_some_and(|n| engine_is_loaded && n == sidecar.id),
last_used: None,
hf_repo: String::new(),
},
disk_usage_bytes: sidecar.size_bytes,
remaining_download_bytes: Some(0),
});
}
out
}
pub(crate) async fn check_model_available(
state: &AppState,
model_name: &str,
) -> Result<Option<ModelPaths>, ApiError> {
{
let cache = state.model_cache.lock().await;
if cache.contains(model_name) {
return Ok(None);
}
}
let paths = {
let config = state.config.read().await;
if config.manifest_model_needs_download(model_name) {
None
} else {
ModelPaths::resolve(model_name, &config)
}
};
if let Some(paths) = paths {
return Ok(Some(paths));
}
{
let current = state.config.read().await.clone();
let fresh_config = current.reload_from_disk_preserving_runtime();
let needs_download = fresh_config.manifest_model_needs_download(model_name);
let paths = if needs_download {
None
} else {
ModelPaths::resolve(model_name, &fresh_config)
};
{
let mut config = state.config.write().await;
*config = fresh_config;
}
if let Some(paths) = paths {
return Ok(Some(paths));
}
}
if looks_like_catalog_id(model_name) {
if let Err(install_err) = install_catalog_model(state, model_name).await {
return Err(install_error_to_api_error(&install_err));
}
let intents = state.catalog_intents.read().await;
let intent = intents
.get(model_name)
.ok_or_else(|| {
ApiError::not_found(format!(
"catalog model '{model_name}' is not installed. Download it from \
the catalog first."
))
})?
.clone();
drop(intents);
let resolved = {
let config = state.config.read().await;
resolve_intent_to_paths(model_name, &intent, &config)
};
match resolved {
Ok(model_cfg) => {
{
let mut config = state.config.write().await;
config.models.insert(model_name.to_string(), model_cfg);
}
let config = state.config.read().await;
if let Some(paths) = ModelPaths::resolve(model_name, &config) {
return Ok(Some(paths));
}
return Err(ApiError::not_found(format!(
"catalog model '{model_name}' resolved to a config that ModelPaths \
could not turn into runtime paths — internal mismatch, please file an issue."
)));
}
Err(e) => return Err(resolve_error_to_api_error(&e)),
}
}
if mold_core::manifest::find_manifest(model_name).is_some() {
return Err(ApiError::not_found(format!(
"model '{model_name}' is not downloaded. Run: mold pull {model_name}"
)));
}
Err(ApiError::unknown_model(format!(
"unknown model '{model_name}'. Run 'mold list' to see available models."
)))
}
pub(crate) async fn ensure_model_ready(
state: &AppState,
model_name: &str,
progress: Option<EngineProgressCallback>,
hint: Option<ActivationHint>,
request_has_lora: bool,
) -> Result<(), ApiError> {
let _guard = state.model_load_lock.lock().await;
{
let mut cache = state.model_cache.lock().await;
let active_vram = cache.active_vram_bytes();
if let Some(entry) = cache.get_mut(model_name) {
if entry.residency == ModelResidency::Gpu {
let must_recreate = entry.engine.model_paths().is_some_and(|paths| {
request_requires_fresh_engine_for_offload_policy(paths, hint, request_has_lora)
});
if must_recreate {
tracing::info!(
model = %model_name,
"recreating loaded engine for request-specific offload policy"
);
} else {
if let Some(callback) = progress.clone() {
entry.engine.set_on_progress(Box::new(move |event| {
callback(event);
}));
} else {
entry.engine.clear_on_progress();
}
return Ok(());
}
}
let cached_paths = entry.engine.model_paths().cloned();
if let Some(paths) = cached_paths.as_ref() {
preflight_memory_guard(model_name, paths, active_vram, 0, hint)?;
}
let load_strategy = cached_paths
.as_ref()
.map(|paths| {
select_server_load_strategy_for_budget(
paths,
effective_load_available_bytes(active_vram, 0),
hint,
)
})
.unwrap_or(mold_inference::LoadStrategy::Eager);
if load_strategy == mold_inference::LoadStrategy::Sequential {
tracing::info!(
model = %model_name,
"server load strategy degraded to sequential to fit memory budget"
);
}
if let Some(active_name) = cache.unload_active() {
#[cfg(feature = "metrics")]
crate::metrics::clear_model_loaded(&active_name);
tracing::info!(
from = %active_name,
to = %model_name,
"unloaded active model to reload cached model"
);
mold_inference::reclaim_gpu_memory(0);
}
let cached = cache.take(model_name).ok_or_else(|| {
ApiError::internal(format!("cache race: model '{model_name}' vanished"))
})?;
drop(cache);
let mut engine = cached.engine;
if load_strategy == mold_inference::LoadStrategy::Sequential {
let Some(paths) = cached_paths else {
let evicted = {
let mut cache = state.model_cache.lock().await;
cache.insert(engine, 0)
};
drop(evicted);
return Err(ApiError::internal(format!(
"cached engine for '{model_name}' does not expose model paths"
)));
};
let config = state.config.read().await;
let offload = server_offload_enabled_for_paths(&paths, hint, request_has_lora);
let resolved_catalog_config =
resolve_installed_catalog_paths_for_worker(model_name, &config)?
.map(|(_, config)| config);
let engine_config = resolved_catalog_config.as_ref().unwrap_or(&config);
match mold_inference::create_engine_with_pool(
model_name.to_string(),
paths,
engine_config,
load_strategy,
0,
offload,
Some(state.shared_pool.clone()),
) {
Ok(new_engine) => {
drop(config);
drop(engine);
engine = new_engine;
}
Err(e) => {
drop(config);
let evicted = {
let mut cache = state.model_cache.lock().await;
cache.insert(engine, 0)
};
drop(evicted);
return Err(ApiError::internal(format!(
"failed to recreate cached engine for '{model_name}': {e}"
)));
}
}
}
if let Some(callback) = progress.clone() {
engine.set_on_progress(Box::new(move |event| {
callback(event);
}));
} else {
engine.clear_on_progress();
}
let model_log = model_name.to_string();
#[cfg(feature = "metrics")]
let load_start = std::time::Instant::now();
let vram_baseline = mold_inference::device::vram_in_use_bytes(0);
let join_result = tokio::task::spawn_blocking(move || {
tracing::info!(model = %model_log, "reloading cached engine...");
if let Err(e) = engine.load() {
tracing::error!("model reload failed: {e:#}");
return Err((
ApiError::internal(format!("model reload error: {e}")),
engine,
));
}
Ok(engine)
})
.await;
match join_result {
Ok(Ok(loaded_engine)) => {
#[cfg(feature = "metrics")]
{
let duration = load_start.elapsed().as_secs_f64();
crate::metrics::record_model_load(model_name, duration);
crate::metrics::set_model_loaded(model_name);
let vram_est = mold_inference::device::vram_in_use_bytes(0);
crate::metrics::record_gpu_memory(vram_est);
}
let vram = mold_inference::device::vram_load_delta(0, vram_baseline);
let evicted = {
let mut cache = state.model_cache.lock().await;
cache.insert(loaded_engine, vram)
};
drop(evicted);
}
Ok(Err((api_err, unloaded_engine))) => {
let evicted = {
let mut cache = state.model_cache.lock().await;
cache.insert(unloaded_engine, 0)
};
drop(evicted);
return Err(api_err);
}
Err(join_err) => {
{
let mut cache = state.model_cache.lock().await;
cache.clear_in_flight(model_name);
}
return Err(ApiError::internal(format!(
"model reload task failed: {join_err}"
)));
}
}
return Ok(());
}
}
match check_model_available(state, model_name).await? {
Some(paths) => create_and_load_engine(state, model_name, paths, progress, hint).await,
None => Ok(()),
}
}
pub(crate) async fn pull_model(
state: &AppState,
model: &str,
progress: Option<DownloadProgressCallback>,
) -> Result<PullStatus, ApiError> {
if mold_core::manifest::find_manifest(&mold_core::manifest::resolve_model_name(model)).is_none()
{
return Err(ApiError::unknown_model(format!(
"unknown model '{model}'. Run 'mold list' to see available models."
)));
}
let _guard = state.pull_lock.lock().await;
{
let config = refresh_config(state).await;
if config.manifest_model_is_downloaded(model) {
return Ok(PullStatus::AlreadyAvailable);
}
}
tracing::info!(model = %model, "pulling model via API");
let opts = mold_core::download::PullOptions::default();
let new_config = match progress {
Some(callback) => {
mold_core::download::pull_and_configure_with_callback(model, callback, &opts)
.await
.map(|(config, _)| config)
}
None => mold_core::download::pull_and_configure(model, &opts)
.await
.map(|(config, _)| config),
}
.map_err(|e| {
tracing::error!("pull failed for {}: {e}", model);
ApiError::internal(format!("failed to pull model '{}': {e}", model))
})?;
{
let mut config = state.config.write().await;
*config = new_config;
}
tracing::info!(model = %model, "pull complete");
Ok(PullStatus::Pulled)
}
pub(crate) async fn unload_model(state: &AppState) -> String {
if let Ok(mut upscaler) = state.upscaler_cache.try_lock() {
if upscaler.is_some() {
*upscaler = None;
tracing::info!("upscaler cache cleared");
}
}
let mut cache = state.model_cache.lock().await;
match cache.unload_active() {
Some(name) => {
#[cfg(feature = "metrics")]
{
crate::metrics::clear_model_loaded(&name);
crate::metrics::record_gpu_memory(0);
}
drop(cache);
mold_inference::reclaim_gpu_memory(0);
tracing::info!(model = %name, "model unloaded via API");
format!("unloaded {name}")
}
None => "no model loaded".to_string(),
}
}
async fn create_and_load_engine(
state: &AppState,
model_name: &str,
paths: ModelPaths,
progress: Option<EngineProgressCallback>,
hint: Option<ActivationHint>,
) -> Result<(), ApiError> {
let active_vram = {
let cache = state.model_cache.lock().await;
cache.active_vram_bytes()
};
preflight_memory_guard(model_name, &paths, active_vram, 0, hint)?;
let load_strategy = select_server_load_strategy_for_device(
&paths,
effective_load_available_bytes(active_vram, 0),
mold_inference::device::total_vram_bytes(0),
hint,
);
if load_strategy == mold_inference::LoadStrategy::Sequential {
tracing::info!(
model = %model_name,
"server load strategy degraded to sequential to fit memory budget"
);
}
let had_active = {
let mut cache = state.model_cache.lock().await;
let result = cache.unload_active();
if let Some(ref name) = result {
#[cfg(feature = "metrics")]
crate::metrics::clear_model_loaded(name);
tracing::info!(
from = %name,
to = %model_name,
"unloading active model before loading new one"
);
}
result.is_some()
};
if had_active {
mold_inference::reclaim_gpu_memory(0);
}
let config = state.config.read().await;
let offload = server_offload_enabled_for_paths(&paths, hint, false);
let mut new_engine = mold_inference::create_engine_with_pool(
model_name.to_string(),
paths,
&config,
load_strategy,
0,
offload,
Some(state.shared_pool.clone()),
)
.map_err(|e| ApiError::internal(format!("failed to create engine for '{model_name}': {e}")))?;
drop(config);
if let Some(callback) = progress {
new_engine.set_on_progress(Box::new(move |event| {
callback(event);
}));
} else {
new_engine.clear_on_progress();
}
let model_log = model_name.to_string();
#[cfg(feature = "metrics")]
let load_start = std::time::Instant::now();
let vram_baseline = mold_inference::device::vram_in_use_bytes(0);
new_engine = tokio::task::spawn_blocking(move || {
tracing::info!(model = %model_log, "loading model...");
new_engine.load().map_err(|e| {
tracing::error!("model load failed: {e:#}");
ApiError::internal(format!("model load error: {e}"))
})?;
Ok::<_, ApiError>(new_engine)
})
.await
.map_err(|e| ApiError::internal(format!("model load task failed: {e}")))??;
#[cfg(feature = "metrics")]
{
let duration = load_start.elapsed().as_secs_f64();
crate::metrics::record_model_load(model_name, duration);
crate::metrics::set_model_loaded(model_name);
}
let vram = mold_inference::device::vram_load_delta(0, vram_baseline);
#[cfg(feature = "metrics")]
crate::metrics::record_gpu_memory(mold_inference::device::vram_in_use_bytes(0));
let evicted = {
let mut cache = state.model_cache.lock().await;
cache.insert(new_engine, vram)
};
drop(evicted);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
const GB: u64 = 1_000_000_000;
struct Ltx2GemmaEnvGuard {
_lock: std::sync::MutexGuard<'static, ()>,
prior_main: Option<std::ffi::OsString>,
prior_legacy: Option<std::ffi::OsString>,
}
impl Drop for Ltx2GemmaEnvGuard {
fn drop(&mut self) {
unsafe {
std::env::remove_var("MOLD_LTX2_GEMMA_DEVICE");
std::env::remove_var("MOLD_LTX2_DEBUG_FORCE_CPU_PROMPT_ENCODER");
if let Some(v) = self.prior_main.take() {
std::env::set_var("MOLD_LTX2_GEMMA_DEVICE", v);
}
if let Some(v) = self.prior_legacy.take() {
std::env::set_var("MOLD_LTX2_DEBUG_FORCE_CPU_PROMPT_ENCODER", v);
}
}
}
}
fn ltx2_gemma_env_guard(value: &str) -> Ltx2GemmaEnvGuard {
use std::sync::{Mutex, OnceLock};
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
let lock = LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|p| p.into_inner());
let prior_main = std::env::var_os("MOLD_LTX2_GEMMA_DEVICE");
let prior_legacy = std::env::var_os("MOLD_LTX2_DEBUG_FORCE_CPU_PROMPT_ENCODER");
unsafe {
std::env::remove_var("MOLD_LTX2_DEBUG_FORCE_CPU_PROMPT_ENCODER");
std::env::set_var("MOLD_LTX2_GEMMA_DEVICE", value);
}
Ltx2GemmaEnvGuard {
_lock: lock,
prior_main,
prior_legacy,
}
}
struct OffloadEnvGuard {
_lock: std::sync::MutexGuard<'static, ()>,
prior: Option<std::ffi::OsString>,
}
impl Drop for OffloadEnvGuard {
fn drop(&mut self) {
unsafe {
std::env::remove_var("MOLD_OFFLOAD");
if let Some(v) = self.prior.take() {
std::env::set_var("MOLD_OFFLOAD", v);
}
}
}
}
fn offload_env_guard(value: &str) -> OffloadEnvGuard {
use std::sync::{Mutex, OnceLock};
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
let lock = LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|p| p.into_inner());
let prior = std::env::var_os("MOLD_OFFLOAD");
unsafe {
std::env::set_var("MOLD_OFFLOAD", value);
}
OffloadEnvGuard { _lock: lock, prior }
}
#[test]
fn installed_catalog_intent_from_sidecar_resolves_downloaded_ltx2_checkpoint() {
let dir = tempfile::tempdir().expect("tempdir");
let install_dir = dir.path().join("cv-2752735");
let primary_rel = "ltx2/civitai/2752735/ltx23_full.safetensors";
let primary = install_dir.join(primary_rel);
std::fs::create_dir_all(primary.parent().unwrap()).unwrap();
std::fs::write(&primary, b"fake").unwrap();
let sidecar = mold_catalog::sidecar::CatalogSidecar {
schema: mold_catalog::sidecar::SIDECAR_SCHEMA,
id: "cv:2752735".to_string(),
source: "civitai".to_string(),
source_id: "2752735".to_string(),
name: "LTX-2.3".to_string(),
author: Some("clueless_engineer".to_string()),
family: "ltx2".to_string(),
family_role: "finetune".to_string(),
sub_family: Some("v2.3".to_string()),
kind: "checkpoint".to_string(),
modality: "video".to_string(),
thumbnail_url: None,
size_bytes: Some(4),
engine_phase: 5,
trained_words: Vec::new(),
primary_filename_rel: primary_rel.to_string(),
written_at: 0,
};
mold_catalog::sidecar::write_sidecar(
&install_dir.join(mold_catalog::sidecar::SIDECAR_FILENAME),
&sidecar,
)
.unwrap();
let intent = installed_catalog_intent_from_sidecar(dir.path(), "cv:2752735")
.expect("installed sidecar should synthesize a catalog intent");
assert_eq!(intent.family, "ltx2");
assert_eq!(intent.primary_recipe_path, primary);
assert!(
intent.companions.iter().any(|c| c.name == "ltx2-te"),
"LTX-2 installed sidecars must still resolve the Gemma companion"
);
}
#[test]
fn installed_catalog_intent_from_sidecar_rejects_text_encoder_primary() {
let dir = tempfile::tempdir().expect("tempdir");
let install_dir = dir.path().join("cv-2442439");
let primary_rel = "z-image/civitai/2442439/zImageTurbo_turbo_txt.safetensors";
let primary = install_dir.join(primary_rel);
std::fs::create_dir_all(primary.parent().unwrap()).unwrap();
std::fs::write(&primary, b"fake").unwrap();
let sidecar = mold_catalog::sidecar::CatalogSidecar {
schema: mold_catalog::sidecar::SIDECAR_SCHEMA,
id: "cv:2442439".to_string(),
source: "civitai".to_string(),
source_id: "2442439".to_string(),
name: "Z Image Turbo".to_string(),
author: None,
family: "z-image".to_string(),
family_role: "finetune".to_string(),
sub_family: Some("turbo".to_string()),
kind: "checkpoint".to_string(),
modality: "image".to_string(),
thumbnail_url: None,
size_bytes: Some(4),
engine_phase: 4,
trained_words: Vec::new(),
primary_filename_rel: primary_rel.to_string(),
written_at: 0,
};
mold_catalog::sidecar::write_sidecar(
&install_dir.join(mold_catalog::sidecar::SIDECAR_FILENAME),
&sidecar,
)
.unwrap();
assert!(
installed_catalog_intent_from_sidecar(dir.path(), "cv:2442439").is_none(),
"stale sidecars that point checkpoint primary at a text encoder must be ignored"
);
}
#[test]
fn installed_catalog_intent_from_sidecar_rejects_te_suffix_primary() {
let dir = tempfile::tempdir().expect("tempdir");
let install_dir = dir.path().join("cv-2597527");
let primary_rel = "flux2/civitai/2597527/qwen38BFluxKlein9BTE_38b.safetensors";
let primary = install_dir.join(primary_rel);
std::fs::create_dir_all(primary.parent().unwrap()).unwrap();
std::fs::write(&primary, b"fake").unwrap();
let sidecar = mold_catalog::sidecar::CatalogSidecar {
schema: mold_catalog::sidecar::SIDECAR_SCHEMA,
id: "cv:2597527".to_string(),
source: "civitai".to_string(),
source_id: "2597527".to_string(),
name: "Qwen 3 8B Flux Klein 9B TE".to_string(),
author: None,
family: "flux2".to_string(),
family_role: "finetune".to_string(),
sub_family: Some("klein-9b".to_string()),
kind: "checkpoint".to_string(),
modality: "image".to_string(),
thumbnail_url: None,
size_bytes: Some(4),
engine_phase: 5,
trained_words: Vec::new(),
primary_filename_rel: primary_rel.to_string(),
written_at: 0,
};
mold_catalog::sidecar::write_sidecar(
&install_dir.join(mold_catalog::sidecar::SIDECAR_FILENAME),
&sidecar,
)
.unwrap();
assert!(
installed_catalog_intent_from_sidecar(dir.path(), "cv:2597527").is_none(),
"checkpoint sidecars whose primary is a TE asset must be ignored"
);
}
fn test_paths_with_total_size(total_bytes: u64) -> (tempfile::TempDir, ModelPaths) {
let dir = tempfile::tempdir().expect("tempdir");
let transformer = dir.path().join("transformer.safetensors");
let vae = dir.path().join("vae.safetensors");
let half = total_bytes / 2;
let rest = total_bytes - half;
let f1 = std::fs::File::create(&transformer).expect("create transformer");
f1.set_len(half).expect("set transformer len");
let f2 = std::fs::File::create(&vae).expect("create vae");
f2.set_len(rest).expect("set vae len");
let paths = ModelPaths {
transformer,
transformer_shards: Vec::new(),
vae,
spatial_upscaler: None,
temporal_upscaler: None,
distilled_lora: None,
t5_encoder: None,
clip_encoder: None,
t5_tokenizer: None,
clip_tokenizer: None,
clip_encoder_2: None,
clip_tokenizer_2: None,
text_encoder_files: Vec::new(),
text_tokenizer: None,
decoder: None,
};
(dir, paths)
}
#[test]
fn test_paths_helper_sets_file_sizes() {
let (_dir, paths) = test_paths_with_total_size(10 * GB);
let peak = mold_inference::device::estimate_peak_memory(
&paths,
mold_inference::LoadStrategy::Eager,
);
assert!(
peak >= 10 * GB,
"expected peak >= 10 GB component sum, got {peak}"
);
assert!(PathBuf::from(&paths.transformer).exists());
assert!(PathBuf::from(&paths.vae).exists());
}
#[test]
fn preflight_uses_active_vram_as_reclaimable() {
let (_dir, paths) = test_paths_with_total_size(10 * GB);
let result =
preflight_memory_guard_with_available("swap-test", &paths, 10 * GB, 8 * GB, None);
assert!(
result.is_ok(),
"expected swap to succeed with reclaimable VRAM, got {result:?}"
);
}
#[test]
fn preflight_rejects_when_peak_exceeds_effective_available() {
let (_dir, paths) = test_paths_with_total_size(20 * GB);
let result = preflight_memory_guard_with_available("too-big", &paths, 5 * GB, 8 * GB, None);
assert!(
result.is_err(),
"expected oversized model to be rejected, got Ok"
);
}
#[test]
fn memory_guard_ok_when_plenty_of_memory() {
assert!(check_model_memory_budget("test-model", 5 * GB, 20 * GB, "").is_ok());
}
#[test]
fn memory_guard_rejects_over_90pct() {
let result = check_model_memory_budget("flux-dev:bf16", 19 * GB, 20 * GB, "Try --offload.");
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.code, "INSUFFICIENT_MEMORY");
assert!(err.error.contains("flux-dev:bf16"));
assert!(err.error.contains("budget cap"));
}
#[test]
fn memory_guard_ok_at_90pct_boundary() {
assert!(check_model_memory_budget("test", 18 * GB, 20 * GB, "").is_ok());
}
#[test]
fn memory_guard_ok_in_warn_zone() {
assert!(check_model_memory_budget("test", 17 * GB, 20 * GB, "").is_ok());
}
#[test]
fn memory_guard_ok_below_warn_zone() {
assert!(check_model_memory_budget("test", 15 * GB, 20 * GB, "").is_ok());
}
#[test]
fn memory_guard_rejects_tiny_available() {
let result = check_model_memory_budget("huge-model", 30 * GB, 16 * GB, "");
assert!(result.is_err());
}
#[test]
fn memory_guard_swap_uses_active_vram_as_reclaimable() {
let free_vram = 2 * GB;
let active_vram = 18 * GB;
let effective = free_vram + active_vram;
assert!(check_model_memory_budget("swap-target", 15 * GB, effective, "").is_ok());
}
#[test]
fn memory_guard_swap_still_rejects_when_oversized() {
let free_vram = GB;
let active_vram = 8 * GB;
let effective = free_vram + active_vram;
assert!(check_model_memory_budget("too-large", 15 * GB, effective, "").is_err());
}
fn flux_shaped_paths_with_sizes(
transformer_gb: u64,
vae_gb: u64,
t5_gb: u64,
clip_gb: u64,
) -> (tempfile::TempDir, ModelPaths) {
let dir = tempfile::tempdir().expect("tempdir");
let mk = |name: &str, sz: u64| {
let p = dir.path().join(name);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
let f = std::fs::File::create(&p).unwrap();
f.set_len(sz * GB).unwrap();
p
};
let transformer = mk("transformer.safetensors", transformer_gb);
let vae = mk("vae.safetensors", vae_gb);
let t5 = mk("t5.safetensors", t5_gb);
let clip = mk("clip.safetensors", clip_gb);
let paths = ModelPaths {
transformer,
transformer_shards: Vec::new(),
vae,
spatial_upscaler: None,
temporal_upscaler: None,
distilled_lora: None,
t5_encoder: Some(t5),
clip_encoder: Some(clip),
t5_tokenizer: None,
clip_tokenizer: None,
clip_encoder_2: None,
clip_tokenizer_2: None,
text_encoder_files: Vec::new(),
text_tokenizer: None,
decoder: None,
};
(dir, paths)
}
#[test]
fn preflight_passes_for_quantized_flux_on_24gb_card_with_swap() {
let (_dir, paths) = flux_shaped_paths_with_sizes(12, 1, 10, 1);
let result = preflight_memory_guard_with_available("flux-dev:q8", &paths, 0, 24 * GB, None);
assert!(
result.is_ok(),
"quantized FLUX must fit on a 24 GB card under the Sequential \
peak estimate (drop-and-reload encoders), got {result:?}"
);
}
#[test]
fn preflight_accepts_forced_flux_offload_bf16_layout_on_24gb() {
let _guard = offload_env_guard("1");
let (_dir, paths) = flux_shaped_paths_with_sizes(24, 1, 10, 1);
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::FluxDit,
};
let result =
preflight_memory_guard_with_available("flux-dev:bf16", &paths, 0, 24 * GB, Some(hint));
assert!(
result.is_ok(),
"forced FLUX offload should use streaming-aware peak instead of \
full BF16 transformer residency, got {result:?}"
);
}
fn sd3_gguf_paths_with_monolithic_vae(
transformer_gb: u64,
vae_gb: u64,
t5_gb: u64,
clip_l_gb: u64,
clip_g_gb: u64,
) -> (tempfile::TempDir, ModelPaths) {
let dir = tempfile::tempdir().expect("tempdir");
let mk = |name: &str, sz: u64| {
let p = dir.path().join(name);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
let f = std::fs::File::create(&p).unwrap();
f.set_len(sz * GB).unwrap();
p
};
let transformer = mk("sd3.5_large-Q8_0.gguf", transformer_gb);
let vae = mk("sd3.5_large.safetensors", vae_gb);
let t5 = mk("t5xxl_fp16.safetensors", t5_gb);
let clip_l = mk("clip_l.safetensors", clip_l_gb);
let clip_g = mk("clip_g.safetensors", clip_g_gb);
let paths = ModelPaths {
transformer,
transformer_shards: Vec::new(),
vae,
spatial_upscaler: None,
temporal_upscaler: None,
distilled_lora: None,
t5_encoder: Some(t5),
clip_encoder: Some(clip_l),
t5_tokenizer: None,
clip_tokenizer: None,
clip_encoder_2: Some(clip_g),
clip_tokenizer_2: None,
text_encoder_files: Vec::new(),
text_tokenizer: None,
decoder: None,
};
(dir, paths)
}
#[test]
fn preflight_accepts_sd3_gguf_with_monolithic_vae_on_24gb() {
let (_dir, paths) = sd3_gguf_paths_with_monolithic_vae(9, 16, 10, 1, 1);
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 2,
dtype_bytes: 2,
family: ActivationFamily::Sd3Mmdit,
};
let result =
preflight_memory_guard_with_available("sd3.5-large:q8", &paths, 0, 24 * GB, Some(hint));
assert!(
result.is_ok(),
"SD3 GGUF should not count the monolithic VAE checkpoint as \
co-resident with the transformer, got {result:?}"
);
}
#[test]
fn server_load_strategy_keeps_sd3_gguf_eager() {
let (_dir, paths) = sd3_gguf_paths_with_monolithic_vae(9, 16, 10, 1, 1);
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 2,
dtype_bytes: 2,
family: ActivationFamily::Sd3Mmdit,
};
let strategy = select_server_load_strategy_for_budget(&paths, Some(32 * GB), Some(hint));
assert_eq!(
strategy,
mold_inference::LoadStrategy::Eager,
"SD3 GGUF has its own quantized runtime path; selecting Sequential \
asks the runtime for unsupported block offload"
);
}
fn zimage_gguf_paths(
transformer_gb: u64,
vae_gb: u64,
text_encoder_gb: u64,
) -> (tempfile::TempDir, ModelPaths) {
let dir = tempfile::tempdir().expect("tempdir");
let mk = |name: &str, sz: u64| {
let p = dir.path().join(name);
let f = std::fs::File::create(&p).unwrap();
f.set_len(sz * GB).unwrap();
p
};
let transformer = mk("z-image-turbo-Q8_0.gguf", transformer_gb);
let vae = mk("vae.safetensors", vae_gb);
let text_encoder = mk("qwen3.safetensors", text_encoder_gb);
let paths = ModelPaths {
transformer,
transformer_shards: Vec::new(),
vae,
spatial_upscaler: None,
temporal_upscaler: None,
distilled_lora: None,
t5_encoder: None,
clip_encoder: None,
t5_tokenizer: None,
clip_tokenizer: None,
clip_encoder_2: None,
clip_tokenizer_2: None,
text_encoder_files: vec![text_encoder],
text_tokenizer: None,
decoder: None,
};
(dir, paths)
}
#[test]
fn server_load_strategy_keeps_zimage_gguf_eager() {
let (_dir, paths) = zimage_gguf_paths(12, 1, 8);
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::ZImageDit,
};
let strategy = select_server_load_strategy_for_budget(&paths, Some(24 * GB), Some(hint));
assert_eq!(
strategy,
mold_inference::LoadStrategy::Eager,
"Z-Image GGUF has a quantized/dense runtime path; selecting Sequential \
asks the runtime for unsupported block offload"
);
}
#[test]
fn offload_env_is_ignored_for_sd3_gguf() {
let _guard = offload_env_guard("1");
let (_dir, paths) = sd3_gguf_paths_with_monolithic_vae(9, 16, 10, 1, 1);
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 2,
dtype_bytes: 2,
family: ActivationFamily::Sd3Mmdit,
};
assert!(
!server_offload_enabled_for_paths(&paths, Some(hint), false),
"global MOLD_OFFLOAD must not force unsupported SD3 GGUF block offload"
);
}
#[test]
fn offload_env_is_ignored_for_zimage_gguf() {
let _guard = offload_env_guard("1");
let (_dir, paths) = zimage_gguf_paths(12, 1, 8);
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::ZImageDit,
};
assert!(
!server_offload_enabled_for_paths(&paths, Some(hint), false),
"global MOLD_OFFLOAD must not force unsupported Z-Image GGUF block offload"
);
}
#[test]
fn offload_env_is_preserved_for_zimage_bf16() {
let _guard = offload_env_guard("1");
let (_dir, paths) = flux_shaped_paths_with_sizes(6, 1, 8, 0);
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::ZImageDit,
};
assert!(
server_offload_enabled_for_paths(&paths, Some(hint), false),
"BF16/FP Z-Image paths should still receive explicit offload"
);
}
#[test]
fn offload_env_is_ignored_for_zimage_lora_with_ambiguous_family_hint() {
let _guard = offload_env_guard("1");
let dir = tempfile::tempdir().expect("tempdir");
let mk = |name: &str, sz: u64| {
let p = dir.path().join(name);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
let f = std::fs::File::create(&p).unwrap();
f.set_len(sz * GB).unwrap();
p
};
let paths = ModelPaths {
transformer: mk("z-image/civitai/2442439/zImageTurbo_turbo.safetensors", 12),
transformer_shards: Vec::new(),
vae: mk("z-image/civitai/2442439/ae_zimgturbo.safetensors", 1),
spatial_upscaler: None,
temporal_upscaler: None,
distilled_lora: None,
t5_encoder: None,
clip_encoder: None,
t5_tokenizer: None,
clip_tokenizer: None,
clip_encoder_2: None,
clip_tokenizer_2: None,
text_encoder_files: vec![mk(
"z-image/civitai/2442439/zImageTurbo_turbo_txt.safetensors",
8,
)],
text_tokenizer: None,
decoder: None,
};
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::FluxDit,
};
assert!(
!server_offload_enabled_for_paths(&paths, Some(hint), true),
"Z-Image LoRA requests must not receive global MOLD_OFFLOAD even \
when duplicate catalog rows provide an ambiguous Flux hint"
);
}
#[test]
fn offload_env_is_ignored_for_flux2_lora_request() {
let _guard = offload_env_guard("1");
let (_dir, paths) = flux2_klein9b_bf16_paths();
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::Flux2Dit,
};
assert!(
!server_offload_enabled_for_paths(&paths, Some(hint), true),
"global MOLD_OFFLOAD must not force Flux.2 block offload for LoRA \
requests because Flux.2 offload+LoRA is not supported"
);
}
#[test]
fn offload_env_is_ignored_for_flux2_lora_with_ambiguous_family_hint() {
let _guard = offload_env_guard("1");
let dir = tempfile::tempdir().expect("tempdir");
let mk = |name: &str, sz: u64| {
let p = dir.path().join(name);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
let f = std::fs::File::create(&p).unwrap();
f.set_len(sz * GB).unwrap();
p
};
let transformer = mk(
"flux2/civitai/2669986/darkBeast_dbkBlitzV15.safetensors",
18,
);
let paths = ModelPaths {
transformer: transformer.clone(),
transformer_shards: vec![transformer],
vae: mk("flux2/civitai/2669986/flux2-vae.safetensors", 1),
spatial_upscaler: None,
temporal_upscaler: None,
distilled_lora: None,
t5_encoder: None,
clip_encoder: None,
t5_tokenizer: None,
clip_tokenizer: None,
clip_encoder_2: None,
clip_tokenizer_2: None,
text_encoder_files: vec![mk("flux2/civitai/2669986/qwen3.safetensors", 16)],
text_tokenizer: None,
decoder: None,
};
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::FluxDit,
};
assert!(
!server_offload_enabled_for_paths(&paths, Some(hint), true),
"Flux.2 LoRA requests must not receive global MOLD_OFFLOAD even \
when the catalog family hint is missing or ambiguous"
);
}
#[test]
fn flux2_lora_request_requires_fresh_engine_when_plain_offload_was_enabled() {
let _guard = offload_env_guard("1");
let dir = tempfile::tempdir().expect("tempdir");
let mk = |name: &str, sz: u64| {
let p = dir.path().join(name);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
let f = std::fs::File::create(&p).unwrap();
f.set_len(sz * GB).unwrap();
p
};
let paths = ModelPaths {
transformer: mk(
"flux2/civitai/2669986/darkBeast_dbkBlitzV15.safetensors",
18,
),
transformer_shards: Vec::new(),
vae: mk("flux2/civitai/2669986/flux2-vae.safetensors", 1),
spatial_upscaler: None,
temporal_upscaler: None,
distilled_lora: None,
t5_encoder: None,
clip_encoder: None,
t5_tokenizer: None,
clip_tokenizer: None,
clip_encoder_2: None,
clip_tokenizer_2: None,
text_encoder_files: vec![mk("flux2/civitai/2669986/qwen3.safetensors", 16)],
text_tokenizer: None,
decoder: None,
};
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::Flux2Dit,
};
assert!(
request_requires_fresh_engine_for_offload_policy(&paths, Some(hint), true),
"a cached Flux.2 engine loaded for plain offload must be recreated \
before serving a LoRA request, otherwise the runtime still sees \
offload+LoRA"
);
}
#[test]
fn offload_env_is_preserved_for_plain_flux2_request() {
let _guard = offload_env_guard("1");
let (_dir, paths) = flux2_klein9b_bf16_paths();
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::Flux2Dit,
};
assert!(
server_offload_enabled_for_paths(&paths, Some(hint), false),
"plain Flux.2 requests should still receive explicit offload"
);
}
#[test]
fn offload_env_is_ignored_for_flux2_gguf() {
let _guard = offload_env_guard("1");
let (dir, mut paths) = flux2_klein9b_bf16_paths();
let gguf = dir.path().join("flux2-klein-9b-q8.gguf");
std::fs::File::create(&gguf)
.unwrap()
.set_len(12 * GB)
.unwrap();
paths.transformer = gguf;
paths.transformer_shards.clear();
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::Flux2Dit,
};
assert!(
!server_offload_enabled_for_paths(&paths, Some(hint), false),
"global MOLD_OFFLOAD must not force Flux.2 GGUF block offload \
because GGUF variants use quantized transformer paths"
);
}
#[test]
fn offload_env_is_ignored_for_flux2_nvfp4() {
let _guard = offload_env_guard("1");
let dir = tempfile::tempdir().expect("tempdir");
let mk = |name: &str, sz: u64| {
let p = dir.path().join(name);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
let f = std::fs::File::create(&p).unwrap();
f.set_len(sz * GB).unwrap();
p
};
let paths = ModelPaths {
transformer: mk(
"flux2/civitai/2759597/miracleinNSFWGeneration_10Nvfp4.safetensors",
18,
),
transformer_shards: Vec::new(),
vae: mk("flux2/civitai/2759597/flux2-vae.safetensors", 1),
spatial_upscaler: None,
temporal_upscaler: None,
distilled_lora: None,
t5_encoder: None,
clip_encoder: None,
t5_tokenizer: None,
clip_tokenizer: None,
clip_encoder_2: None,
clip_tokenizer_2: None,
text_encoder_files: vec![mk("flux2/civitai/2759597/qwen3.safetensors", 8)],
text_tokenizer: None,
decoder: None,
};
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::Flux2Dit,
};
assert!(
!server_offload_enabled_for_paths(&paths, Some(hint), false),
"global MOLD_OFFLOAD must not force Flux.2 NVFP4 block offload \
because the runtime does not support NVFP4 streaming layers"
);
}
fn qwen_image_q8_paths(
transformer_gb: u64,
vae_gb: u64,
text_encoder_gb: u64,
) -> (tempfile::TempDir, ModelPaths) {
let dir = tempfile::tempdir().expect("tempdir");
let mk = |name: &str, sz: u64| {
let p = dir.path().join(name);
let f = std::fs::File::create(&p).unwrap();
f.set_len(sz * GB).unwrap();
p
};
let transformer = mk("qwen-image-Q8_0.gguf", transformer_gb);
let vae = mk("qwen-image-vae.safetensors", vae_gb);
let text_encoder = mk("qwen2.5-vl.safetensors", text_encoder_gb);
let paths = ModelPaths {
transformer,
transformer_shards: Vec::new(),
vae,
spatial_upscaler: None,
temporal_upscaler: None,
distilled_lora: None,
t5_encoder: None,
clip_encoder: None,
t5_tokenizer: None,
clip_tokenizer: None,
clip_encoder_2: None,
clip_tokenizer_2: None,
text_encoder_files: vec![text_encoder],
text_tokenizer: None,
decoder: None,
};
(dir, paths)
}
#[test]
fn preflight_accepts_quantized_qwen_image_q8_on_24gb() {
let (_dir, paths) = qwen_image_q8_paths(21, 1, 16);
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 2,
dtype_bytes: 2,
family: ActivationFamily::QwenImageDit,
};
let result =
preflight_memory_guard_with_available("qwen-image:q8", &paths, 0, 24 * GB, Some(hint));
assert!(
result.is_ok(),
"Qwen-Image GGUF Q8 should be admitted on 24 GB because the runtime \
uses split-CFG and staged text/VAE phases instead of the generic \
full-headroom sequential estimate, got {result:?}"
);
}
#[test]
fn server_load_strategy_uses_sequential_for_zimage_requests() {
let (_dir, paths) = flux_shaped_paths_with_sizes(6, 1, 8, 0);
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::ZImageDit,
};
let strategy = select_server_load_strategy_for_budget(&paths, Some(24 * GB), Some(hint));
assert_eq!(
strategy,
mold_inference::LoadStrategy::Sequential,
"Z-Image server requests should use staged loading so base/source/LoRA \
share the same memory contract"
);
}
#[test]
fn eager_strategy_would_have_rejected_quantized_flux_on_24gb() {
let (_dir, paths) = flux_shaped_paths_with_sizes(12, 1, 10, 1);
let eager_peak = mold_inference::device::estimate_peak_memory(
&paths,
mold_inference::LoadStrategy::Eager,
);
let hard_limit = (24 * GB) * 9 / 10;
assert!(
eager_peak > hard_limit,
"Eager peak ({eager_peak}) should exceed hard limit ({hard_limit}) — \
this is the false-rejection the Sequential switch fixes"
);
}
#[test]
fn server_load_strategy_degrades_when_only_sequential_fits() {
let (_dir, paths) = flux_shaped_paths_with_sizes(12, 1, 10, 1);
let strategy = select_server_load_strategy_for_budget(&paths, Some(24 * GB), None);
assert_eq!(
strategy,
mold_inference::LoadStrategy::Sequential,
"server load should match the sequential preflight assumption instead \
of eager-loading a model whose summed components exceed the budget"
);
}
#[test]
fn server_load_strategy_stays_eager_when_eager_fits() {
let (_dir, paths) = flux_shaped_paths_with_sizes(8, 1, 2, 1);
let strategy = select_server_load_strategy_for_budget(&paths, Some(24 * GB), None);
assert_eq!(strategy, mold_inference::LoadStrategy::Eager);
}
#[test]
fn server_load_strategy_stays_eager_when_no_budget_available() {
let (_dir, paths) = flux_shaped_paths_with_sizes(12, 1, 10, 1);
let strategy = select_server_load_strategy_for_budget(&paths, None, None);
assert_eq!(strategy, mold_inference::LoadStrategy::Eager);
}
#[test]
fn preflight_memory_guard_accepts_resolution_for_activation_budget() {
let _guard = offload_env_guard("0");
let (_dir, paths) = flux_shaped_paths_with_sizes(23, 1, 9, 1);
let hint_768 = ActivationHint {
width: 768,
height: 768,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::FluxDit,
};
let hint_2048 = ActivationHint {
width: 2048,
height: 2048,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::FluxDit,
};
let card_total = 30 * GB;
let result_768 = preflight_memory_guard_with_available(
"flux-dev",
&paths,
0,
card_total,
Some(hint_768),
);
let result_2048 = preflight_memory_guard_with_available(
"flux-dev",
&paths,
0,
card_total,
Some(hint_2048),
);
assert!(
result_768.is_ok(),
"768² FLUX should fit on 30 GB (small activation budget), got {result_768:?}"
);
assert!(
result_2048.is_err(),
"2048² FLUX must be rejected on 30 GB (large activation budget pushes \
peak past 90 % cap), got {result_2048:?}"
);
}
fn flux2_klein9b_bf16_paths() -> (tempfile::TempDir, ModelPaths) {
let dir = tempfile::tempdir().expect("tempdir");
let mk = |name: &str, sz: u64| {
let p = dir.path().join(name);
let f = std::fs::File::create(&p).unwrap();
f.set_len(sz * GB).unwrap();
p
};
let shard_a = mk("diffusion_pytorch_model-00001-of-00002.safetensors", 10);
let shard_b = mk("diffusion_pytorch_model-00002-of-00002.safetensors", 8);
let vae = mk("flux2-vae.safetensors", 1);
let te_a = mk("text_encoder-00001-of-00004.safetensors", 5);
let te_b = mk("text_encoder-00002-of-00004.safetensors", 5);
let te_c = mk("text_encoder-00003-of-00004.safetensors", 5);
let te_d = mk("text_encoder-00004-of-00004.safetensors", 1);
let paths = ModelPaths {
transformer: shard_a.clone(),
transformer_shards: vec![shard_a, shard_b],
vae,
spatial_upscaler: None,
temporal_upscaler: None,
distilled_lora: None,
t5_encoder: None,
clip_encoder: None,
t5_tokenizer: None,
clip_tokenizer: None,
clip_encoder_2: None,
clip_tokenizer_2: None,
text_encoder_files: vec![te_a, te_b, te_c, te_d],
text_tokenizer: None,
decoder: None,
};
(dir, paths)
}
fn flux2_large_bf16_paths_with_quantized_encoder() -> (tempfile::TempDir, ModelPaths) {
let dir = tempfile::tempdir().expect("tempdir");
let mk = |name: &str, sz: u64| {
let p = dir.path().join(name);
let f = std::fs::File::create(&p).unwrap();
f.set_len(sz * GB).unwrap();
p
};
let shard_a = mk("diffusion_pytorch_model-00001-of-00002.safetensors", 10);
let shard_b = mk("diffusion_pytorch_model-00002-of-00002.safetensors", 8);
let vae = mk("flux2-vae.safetensors", 1);
let qwen3_q3 = mk("qwen3-q3.gguf", 3);
let paths = ModelPaths {
transformer: shard_a.clone(),
transformer_shards: vec![shard_a, shard_b],
vae,
spatial_upscaler: None,
temporal_upscaler: None,
distilled_lora: None,
t5_encoder: None,
clip_encoder: None,
t5_tokenizer: None,
clip_tokenizer: None,
clip_encoder_2: None,
clip_tokenizer_2: None,
text_encoder_files: vec![qwen3_q3],
text_tokenizer: None,
decoder: None,
};
(dir, paths)
}
#[test]
fn preflight_allows_flux2_klein9b_bf16_on_24gb_when_sequential_budget_fits() {
let (_dir, paths) = flux2_klein9b_bf16_paths();
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::Flux2Dit,
};
let result = preflight_memory_guard_with_available(
"flux2-klein-9b:bf16",
&paths,
0,
24 * GB,
Some(hint),
);
assert!(
result.is_ok(),
"Klein-9B BF16 should be admitted on a 24 GB card when the \
sequential transformer/VAE phase plus activation budget fits; \
Qwen3 can be quantized/dropped before denoise, got {result:?}"
);
}
#[test]
fn preflight_rejects_flux2_klein9b_bf16_on_24gb_when_activation_budget_exceeds_cap() {
let (_dir, paths) = flux2_klein9b_bf16_paths();
let hint = ActivationHint {
width: 2048,
height: 2048,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::Flux2Dit,
};
let result = preflight_memory_guard_with_available(
"flux2-klein-9b:bf16",
&paths,
0,
24 * GB,
Some(hint),
);
assert!(
result.is_err(),
"Klein-9B BF16 should still reject when resolution-scaled \
activation budget pushes the sequential phase past the 90% cap, got {result:?}"
);
}
#[test]
fn server_load_strategy_degrades_flux2_klein9b_bf16_on_24gb_to_sequential() {
let (_dir, paths) = flux2_klein9b_bf16_paths();
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::Flux2Dit,
};
let strategy = select_server_load_strategy_for_budget(&paths, Some(24 * GB), Some(hint));
assert_eq!(
strategy,
mold_inference::LoadStrategy::Sequential,
"server must use load-use-drop for Klein-9B BF16 on 24 GB so the \
text encoder is not co-resident with the transformer"
);
}
#[test]
fn server_load_strategy_degrades_large_flux2_bf16_even_with_quantized_encoder() {
let (_dir, paths) = flux2_large_bf16_paths_with_quantized_encoder();
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::Flux2Dit,
};
let strategy = select_server_load_strategy_for_budget(&paths, Some(24 * GB), Some(hint));
assert_eq!(
strategy,
mold_inference::LoadStrategy::Sequential,
"large Flux.2 BF16 transformer shards need load-use-drop on 24 GB \
even when Qwen3 resolves to a small quantized encoder"
);
}
#[test]
fn server_load_strategy_forces_klein9b_bf16_sequential_on_24gb_even_with_overgenerous_budget() {
let (_dir, paths) = flux2_klein9b_bf16_paths();
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::Flux2Dit,
};
let strategy = select_server_load_strategy_for_device(
&paths,
Some(128 * GB),
Some(24 * GB),
Some(hint),
);
assert_eq!(
strategy,
mold_inference::LoadStrategy::Sequential,
"Klein-9B BF16 must not use eager loading on 24 GB cards even if \
the live free-memory query is over-generous or falls back to \
system memory"
);
}
#[test]
fn server_load_strategy_caps_overgenerous_budget_for_klein_like_bf16_model() {
let (_dir, paths) = flux2_klein9b_bf16_paths();
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::Flux2Dit,
};
let strategy = select_server_load_strategy_for_device(
&paths,
Some(128 * GB),
Some(24 * GB),
Some(hint),
);
assert_eq!(
strategy,
mold_inference::LoadStrategy::Sequential,
"Klein-9B-shaped BF16 loads must use device VRAM as the budget cap \
even when the live available-memory reading falls back to a larger \
system-memory value"
);
}
#[test]
fn server_load_strategy_uses_device_total_when_live_available_missing() {
let (_dir, paths) = flux2_klein9b_bf16_paths();
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::Flux2Dit,
};
let strategy =
select_server_load_strategy_for_device(&paths, None, Some(24 * GB), Some(hint));
assert_eq!(
strategy,
mold_inference::LoadStrategy::Sequential,
"when live free-memory probing is unavailable, the worker should still \
use known device total VRAM instead of defaulting to eager"
);
}
fn ltx2_shaped_paths_with_sizes(
transformer_gb: u64,
gemma_te_gb: u64,
) -> (tempfile::TempDir, ModelPaths) {
let dir = tempfile::tempdir().expect("tempdir");
let mk = |name: &str, sz: u64| {
let p = dir.path().join(name);
let f = std::fs::File::create(&p).unwrap();
f.set_len(sz * GB).unwrap();
p
};
let transformer = mk("ltx2_full.safetensors", transformer_gb);
let gemma = mk("gemma_te.safetensors", gemma_te_gb);
let paths = ModelPaths {
transformer: transformer.clone(),
transformer_shards: Vec::new(),
vae: transformer,
spatial_upscaler: None,
temporal_upscaler: None,
distilled_lora: None,
t5_encoder: None,
clip_encoder: None,
t5_tokenizer: None,
clip_tokenizer: None,
clip_encoder_2: None,
clip_tokenizer_2: None,
text_encoder_files: vec![gemma],
text_tokenizer: None,
decoder: None,
};
(dir, paths)
}
#[test]
fn preflight_accepts_ltx2_22b_on_24gb_card_via_streaming_peak() {
let (_dir, paths) = ltx2_shaped_paths_with_sizes(46, 0);
let hint = ActivationHint {
width: 768,
height: 512,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::Ltx2Video,
};
let result =
preflight_memory_guard_with_available("cv:2752735", &paths, 0, 24 * GB, Some(hint));
assert!(
result.is_ok(),
"22B LTX-2 must fit on a 24 GB card under streaming-aware peak \
(only ~2 blocks co-resident; runtime handles its own memory), \
got {result:?}",
);
}
#[test]
fn preflight_accepts_ltx2_22b_by_catalog_path_when_hint_is_missing() {
let dir = tempfile::tempdir().expect("tempdir");
let mk = |name: &str, sz: u64| {
let p = dir.path().join(name);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
let f = std::fs::File::create(&p).unwrap();
f.set_len(sz * GB).unwrap();
p
};
let transformer = mk("ltx2/civitai/2752735/ltx23_full.safetensors", 46);
let paths = ModelPaths {
transformer: transformer.clone(),
transformer_shards: Vec::new(),
vae: transformer,
spatial_upscaler: None,
temporal_upscaler: None,
distilled_lora: None,
t5_encoder: None,
clip_encoder: None,
t5_tokenizer: None,
clip_tokenizer: None,
clip_encoder_2: None,
clip_tokenizer_2: None,
text_encoder_files: Vec::new(),
text_tokenizer: None,
decoder: None,
};
let result = preflight_memory_guard_with_available("cv:2752735", &paths, 0, 24 * GB, None);
assert!(
result.is_ok(),
"LTX-2 catalog paths should use the streaming-transformer peak even \
when the multi-GPU worker cannot resolve a family hint, got {result:?}"
);
}
#[test]
fn preflight_rejects_ltx2_22b_when_hint_marks_non_streaming() {
let _guard = offload_env_guard("0");
let (_dir, paths) = ltx2_shaped_paths_with_sizes(46, 0);
let hint = ActivationHint {
width: 768,
height: 512,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::FluxDit,
};
let result =
preflight_memory_guard_with_available("cv:2752735", &paths, 0, 24 * GB, Some(hint));
assert!(
result.is_err(),
"without the LTX-2 streaming hint the file-size peak must reject \
a 46 GB transformer on a 24 GB card — this anchors the regression \
that landed before the streaming-aware path",
);
}
#[test]
fn preflight_rejects_ltx2_when_encoder_phase_exceeds_card() {
let _guard = ltx2_gemma_env_guard("gpu");
let (_dir, paths) = ltx2_shaped_paths_with_sizes(46, 25);
let hint = ActivationHint {
width: 768,
height: 512,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::Ltx2Video,
};
let result =
preflight_memory_guard_with_available("cv:2752735", &paths, 0, 24 * GB, Some(hint));
assert!(
result.is_err(),
"25 GB Gemma TE alone exceeds 90 %% of 24 GB during the encoder \
phase — preflight must surface this even when the transformer \
is streamed, got {result:?}",
);
}
#[test]
fn preflight_admits_ltx2_auto_gemma_even_when_gpu_encoder_would_exceed_cap() {
let _guard = ltx2_gemma_env_guard("auto");
let (_dir, paths) = ltx2_shaped_paths_with_sizes(46, 25);
let hint = ActivationHint {
width: 768,
height: 512,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::Ltx2Video,
};
let result =
preflight_memory_guard_with_available("cv:2752735", &paths, 0, 24 * GB, Some(hint));
assert!(
result.is_ok(),
"auto Gemma placement can fall back to CPU at runtime, so preflight \
must not reject solely because a same-GPU prompt encoder phase \
would exceed the hard cap, got {result:?}",
);
}
#[test]
fn preflight_admits_ltx2_22b_with_25gb_gemma_when_resolver_picks_cpu() {
let _guard = ltx2_gemma_env_guard("cpu");
let (_dir, paths) = ltx2_shaped_paths_with_sizes(46, 25);
let hint = ActivationHint {
width: 768,
height: 512,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::Ltx2Video,
};
let result =
preflight_memory_guard_with_available("cv:2752735", &paths, 0, 24 * GB, Some(hint));
assert!(
result.is_ok(),
"with MOLD_LTX2_GEMMA_DEVICE=cpu the encoder phase should not \
count against GPU VRAM, so cv:2752735 must admit on 24 GB even \
with a 25 GB Gemma TE, got {result:?}",
);
}
#[test]
fn activation_hint_from_request_classifies_correctly() {
let mut req = GenerateRequest {
prompt: "test".into(),
negative_prompt: None,
model: "flux-dev:bf16".into(),
width: 1024,
height: 1024,
steps: 20,
guidance: 3.5,
seed: None,
batch_size: 1,
output_format: Default::default(),
embed_metadata: None,
scheduler: None,
cfg_plus: None,
source_image: None,
edit_images: None,
strength: 1.0,
mask_image: None,
control_image: None,
control_model: None,
control_scale: 1.0,
expand: None,
original_prompt: None,
lora: None,
frames: None,
fps: None,
upscale_model: None,
gif_preview: false,
enable_audio: None,
audio_file: None,
audio_file_path: None,
source_video: None,
source_video_path: None,
keyframes: None,
pipeline: None,
loras: None,
retake_range: None,
spatial_upscale: None,
temporal_upscale: None,
placement: None,
};
let hint_flux = ActivationHint::from_request(&req, "flux");
assert_eq!(hint_flux.family, ActivationFamily::FluxDit);
assert_eq!(hint_flux.batch, 1);
let hint_sdxl = ActivationHint::from_request(&req, "sdxl");
assert_eq!(hint_sdxl.family, ActivationFamily::SdxlUnet);
assert_eq!(hint_sdxl.batch, 2);
req.guidance = 1.0;
let hint_sdxl_lcm = ActivationHint::from_request(&req, "sdxl");
assert_eq!(hint_sdxl_lcm.batch, 1);
let hint_unknown = ActivationHint::from_request(&req, "totally-bogus");
assert_eq!(hint_unknown.family, ActivationFamily::FluxDit);
}
#[test]
fn copy_catalog_companion_flux_vae_populates_when_unset() {
let mut cfg = mold_core::ModelConfig {
family: Some("flux".into()),
transformer: Some("/m/cv-x/flux/civitai/x/unet.safetensors".into()),
vae: None,
..Default::default()
};
let paths = ModelPaths {
transformer: PathBuf::from("/m/flux-vae/ae.safetensors"),
transformer_shards: Vec::new(),
vae: PathBuf::from("/m/flux-vae/ae.safetensors"),
spatial_upscaler: None,
temporal_upscaler: None,
distilled_lora: None,
t5_encoder: None,
clip_encoder: None,
t5_tokenizer: None,
clip_tokenizer: None,
clip_encoder_2: None,
clip_tokenizer_2: None,
text_encoder_files: Vec::new(),
text_tokenizer: None,
decoder: None,
};
copy_catalog_companion(&mut cfg, "flux-vae", &paths);
assert_eq!(
cfg.vae.as_deref(),
Some("/m/flux-vae/ae.safetensors"),
"transformer-only cfg.vae must be populated from the flux-vae companion path"
);
}
#[test]
fn copy_catalog_companion_flux_vae_does_not_override_bundled() {
let mut cfg = mold_core::ModelConfig {
family: Some("flux".into()),
transformer: Some("/m/cv-x/flux/civitai/x/full.safetensors".into()),
vae: Some("/m/cv-x/flux/civitai/x/full.safetensors".into()),
..Default::default()
};
let paths = ModelPaths {
transformer: PathBuf::from("/m/flux-vae/ae.safetensors"),
transformer_shards: Vec::new(),
vae: PathBuf::from("/m/flux-vae/ae.safetensors"),
spatial_upscaler: None,
temporal_upscaler: None,
distilled_lora: None,
t5_encoder: None,
clip_encoder: None,
t5_tokenizer: None,
clip_tokenizer: None,
clip_encoder_2: None,
clip_tokenizer_2: None,
text_encoder_files: Vec::new(),
text_tokenizer: None,
decoder: None,
};
copy_catalog_companion(&mut cfg, "flux-vae", &paths);
assert_eq!(
cfg.vae.as_deref(),
Some("/m/cv-x/flux/civitai/x/full.safetensors"),
"bundled cfg.vae must survive the flux-vae companion pass"
);
}
fn write_safetensors_with_keys(path: &std::path::Path, keys: &[&str]) {
use std::io::Write;
let mut header = serde_json::Map::new();
for key in keys {
header.insert(
(*key).to_string(),
serde_json::json!({
"dtype": "F32",
"shape": [1],
"data_offsets": [0, 4],
}),
);
}
let header_json = serde_json::to_vec(&serde_json::Value::Object(header)).unwrap();
let mut f = std::fs::File::create(path).expect("create fixture");
f.write_all(&(header_json.len() as u64).to_le_bytes())
.unwrap();
f.write_all(&header_json).unwrap();
f.write_all(&[0u8; 4]).unwrap();
}
fn flux_unet_only_catalog_entry(
version_id: &str,
file_name: &str,
) -> mold_catalog::entry::CatalogEntry {
use mold_catalog::entry::{
CatalogEntry, CatalogId, DownloadRecipe, FamilyRole, FileFormat, LicenseFlags,
Modality, RecipeFile, Source, TokenKind,
};
use mold_catalog::families::Family;
CatalogEntry {
id: CatalogId::from(format!("cv:{version_id}")),
source: Source::Civitai,
source_id: version_id.to_string(),
name: "FLUX Unet-only fine-tune".into(),
author: Some("someone".into()),
family: Family::Flux,
family_role: FamilyRole::Finetune,
sub_family: None,
modality: Modality::Image,
kind: mold_catalog::entry::Kind::Checkpoint,
file_format: FileFormat::Safetensors,
bundling: mold_catalog::entry::Bundling::SingleFile,
size_bytes: Some(12_000_000_000),
download_count: 0,
rating: None,
likes: 0,
nsfw: false,
thumbnail_url: None,
description: None,
license: None,
license_flags: LicenseFlags::default(),
tags: vec![],
companions: vec!["t5-v1_1-xxl".into(), "clip-l".into(), "flux-vae".into()],
download_recipe: DownloadRecipe {
files: vec![RecipeFile {
url: format!("https://civitai.com/api/download/models/{version_id}"),
dest: format!("{{family}}/civitai/{version_id}/{file_name}"),
sha256: Some("DEAD".repeat(16)),
size_bytes: Some(12_000_000_000),
role: None,
}],
needs_token: Some(TokenKind::Civitai),
},
engine_phase: 1,
created_at: None,
updated_at: None,
added_at: 0,
trained_words: vec![],
}
}
#[test]
fn synthesize_catalog_config_uses_flux_vae_companion_when_bundle_lacks_vae() {
let dir = tempfile::tempdir().unwrap();
let models_dir = dir.path();
let _saved = std::env::var("MOLD_HOME").ok();
unsafe { std::env::set_var("MOLD_HOME", models_dir.to_string_lossy().as_ref()) };
let primary_path = models_dir
.join("cv-994561/flux/civitai/994561/realHornyProV3_realHornyProV3Unet.safetensors");
std::fs::create_dir_all(primary_path.parent().unwrap()).unwrap();
write_safetensors_with_keys(
&primary_path,
&[
"double_blocks.0.img_attn.proj.weight",
"single_blocks.0.linear1.weight",
"img_in.weight",
],
);
let vae_path = models_dir.join("flux-vae/ae.safetensors");
std::fs::create_dir_all(vae_path.parent().unwrap()).unwrap();
std::fs::File::create(&vae_path).unwrap();
let mut config = mold_core::Config {
models_dir: models_dir.to_string_lossy().into_owned(),
..Default::default()
};
config.models.insert(
"flux-vae".into(),
mold_core::ModelConfig {
family: Some("companion".into()),
transformer: Some(vae_path.to_string_lossy().into_owned()),
vae: Some(vae_path.to_string_lossy().into_owned()),
..Default::default()
},
);
config.models.insert(
"clip-l".into(),
mold_core::ModelConfig {
family: Some("companion".into()),
transformer: Some(format!("{}/clip-l/model.safetensors", models_dir.display())),
vae: Some(format!("{}/clip-l/model.safetensors", models_dir.display())),
..Default::default()
},
);
config.models.insert(
"t5-v1_1-xxl".into(),
mold_core::ModelConfig {
family: Some("companion".into()),
transformer: Some(format!(
"{}/t5-v1_1-xxl/t5xxl_fp16.safetensors",
models_dir.display()
)),
vae: Some(format!(
"{}/t5-v1_1-xxl/t5xxl_fp16.safetensors",
models_dir.display()
)),
..Default::default()
},
);
let entry =
flux_unet_only_catalog_entry("994561", "realHornyProV3_realHornyProV3Unet.safetensors");
let synth = synthesize_catalog_config(&entry, models_dir, &config).unwrap();
assert_eq!(
synth.transformer.as_deref(),
primary_path.to_str(),
"transformer must point at the on-disk primary checkpoint"
);
assert_eq!(
synth.vae.as_deref(),
vae_path.to_str(),
"flux-vae companion must populate cfg.vae for transformer-only checkpoints",
);
unsafe {
match _saved {
Some(v) => std::env::set_var("MOLD_HOME", v),
None => std::env::remove_var("MOLD_HOME"),
}
}
}
#[test]
fn synthesize_catalog_config_uses_bundled_vae_when_present() {
let dir = tempfile::tempdir().unwrap();
let models_dir = dir.path();
let _saved = std::env::var("MOLD_HOME").ok();
unsafe { std::env::set_var("MOLD_HOME", models_dir.to_string_lossy().as_ref()) };
let primary_path = models_dir.join("cv-101010/flux/civitai/101010/flux_full.safetensors");
std::fs::create_dir_all(primary_path.parent().unwrap()).unwrap();
write_safetensors_with_keys(
&primary_path,
&[
"model.diffusion_model.double_blocks.0.img_attn.proj.weight",
"first_stage_model.encoder.conv_in.weight",
],
);
let vae_path = models_dir.join("flux-vae/ae.safetensors");
std::fs::create_dir_all(vae_path.parent().unwrap()).unwrap();
std::fs::File::create(&vae_path).unwrap();
let mut config = mold_core::Config {
models_dir: models_dir.to_string_lossy().into_owned(),
..Default::default()
};
config.models.insert(
"flux-vae".into(),
mold_core::ModelConfig {
family: Some("companion".into()),
transformer: Some(vae_path.to_string_lossy().into_owned()),
vae: Some(vae_path.to_string_lossy().into_owned()),
..Default::default()
},
);
config.models.insert(
"clip-l".into(),
mold_core::ModelConfig {
family: Some("companion".into()),
transformer: Some(format!("{}/clip-l/model.safetensors", models_dir.display())),
vae: Some(format!("{}/clip-l/model.safetensors", models_dir.display())),
..Default::default()
},
);
config.models.insert(
"t5-v1_1-xxl".into(),
mold_core::ModelConfig {
family: Some("companion".into()),
transformer: Some(format!(
"{}/t5-v1_1-xxl/t5xxl_fp16.safetensors",
models_dir.display()
)),
vae: Some(format!(
"{}/t5-v1_1-xxl/t5xxl_fp16.safetensors",
models_dir.display()
)),
..Default::default()
},
);
let entry = flux_unet_only_catalog_entry("101010", "flux_full.safetensors");
let synth = synthesize_catalog_config(&entry, models_dir, &config).unwrap();
assert_eq!(synth.transformer.as_deref(), primary_path.to_str());
assert_eq!(
synth.vae.as_deref(),
primary_path.to_str(),
"bundled-VAE checkpoint must keep cfg.vae == primary; flux-vae companion is a no-op",
);
unsafe {
match _saved {
Some(v) => std::env::set_var("MOLD_HOME", v),
None => std::env::remove_var("MOLD_HOME"),
}
}
}
fn stub_flux_companion_paths_in_dir(
config: &mut mold_core::Config,
models_dir: &std::path::Path,
flux_vae_present: bool,
) {
let vae_path = models_dir.join("flux-vae/ae.safetensors");
std::fs::create_dir_all(vae_path.parent().unwrap()).unwrap();
if flux_vae_present {
std::fs::File::create(&vae_path).unwrap();
}
config.models.insert(
"flux-vae".into(),
mold_core::ModelConfig {
family: Some("companion".into()),
transformer: Some(vae_path.to_string_lossy().into_owned()),
vae: Some(vae_path.to_string_lossy().into_owned()),
..Default::default()
},
);
let clip_path = models_dir.join("clip-l/model.safetensors");
std::fs::create_dir_all(clip_path.parent().unwrap()).unwrap();
std::fs::File::create(&clip_path).unwrap();
config.models.insert(
"clip-l".into(),
mold_core::ModelConfig {
family: Some("companion".into()),
transformer: Some(clip_path.to_string_lossy().into_owned()),
vae: Some(clip_path.to_string_lossy().into_owned()),
clip_tokenizer: Some(format!("{}/clip-l/tokenizer.json", models_dir.display())),
..Default::default()
},
);
let t5_path = models_dir.join("t5-v1_1-xxl/t5xxl_fp16.safetensors");
std::fs::create_dir_all(t5_path.parent().unwrap()).unwrap();
std::fs::File::create(&t5_path).unwrap();
config.models.insert(
"t5-v1_1-xxl".into(),
mold_core::ModelConfig {
family: Some("companion".into()),
transformer: Some(t5_path.to_string_lossy().into_owned()),
vae: Some(t5_path.to_string_lossy().into_owned()),
t5_tokenizer: Some(format!(
"{}/t5-v1_1-xxl/tokenizer.json",
models_dir.display()
)),
..Default::default()
},
);
}
#[test]
fn synthesis_intent_is_consistent_before_and_after_download() {
let dir = tempfile::tempdir().unwrap();
let models_dir = dir.path();
let entry =
flux_unet_only_catalog_entry("994561", "realHornyProV3_realHornyProV3Unet.safetensors");
let intent_absent = mold_catalog::synthesis::synthesize_intent(&entry, models_dir).unwrap();
let primary_path = models_dir
.join("cv-994561/flux/civitai/994561/realHornyProV3_realHornyProV3Unet.safetensors");
std::fs::create_dir_all(primary_path.parent().unwrap()).unwrap();
write_safetensors_with_keys(
&primary_path,
&[
"double_blocks.0.img_attn.proj.weight",
"single_blocks.0.linear1.weight",
"img_in.weight",
],
);
let intent_present =
mold_catalog::synthesis::synthesize_intent(&entry, models_dir).unwrap();
assert_eq!(
intent_absent, intent_present,
"intent synthesis must be pure — independent of disk state"
);
}
#[test]
fn resolve_intent_picks_flux_vae_companion_when_primary_is_transformer_only() {
let dir = tempfile::tempdir().unwrap();
let models_dir = dir.path();
let _saved = std::env::var("MOLD_HOME").ok();
unsafe { std::env::set_var("MOLD_HOME", models_dir.to_string_lossy().as_ref()) };
let primary_path = models_dir
.join("cv-994561/flux/civitai/994561/realHornyProV3_realHornyProV3Unet.safetensors");
std::fs::create_dir_all(primary_path.parent().unwrap()).unwrap();
write_safetensors_with_keys(
&primary_path,
&[
"double_blocks.0.img_attn.proj.weight",
"single_blocks.0.linear1.weight",
"img_in.weight",
],
);
let mut config = mold_core::Config {
models_dir: models_dir.to_string_lossy().into_owned(),
..Default::default()
};
stub_flux_companion_paths_in_dir(&mut config, models_dir, true);
let entry =
flux_unet_only_catalog_entry("994561", "realHornyProV3_realHornyProV3Unet.safetensors");
let intent = mold_catalog::synthesis::synthesize_intent(&entry, models_dir).unwrap();
let cfg = resolve_intent_to_paths("cv:994561", &intent, &config).unwrap();
let vae_path = models_dir.join("flux-vae/ae.safetensors");
assert_eq!(cfg.vae.as_deref(), vae_path.to_str());
assert_eq!(cfg.transformer.as_deref(), primary_path.to_str());
unsafe {
match _saved {
Some(v) => std::env::set_var("MOLD_HOME", v),
None => std::env::remove_var("MOLD_HOME"),
}
}
}
#[test]
fn resolve_intent_preserves_flux_schnell_subfamily() {
let dir = tempfile::tempdir().unwrap();
let models_dir = dir.path();
let _saved = std::env::var("MOLD_HOME").ok();
unsafe { std::env::set_var("MOLD_HOME", models_dir.to_string_lossy().as_ref()) };
let primary_path = models_dir
.join("cv-1153358/flux/civitai/1153358/agfluxSchnell_realistic23.safetensors");
std::fs::create_dir_all(primary_path.parent().unwrap()).unwrap();
write_safetensors_with_keys(
&primary_path,
&[
"model.diffusion_model.double_blocks.0.img_attn.proj.weight",
"model.diffusion_model.img_in.weight",
],
);
let mut config = mold_core::Config {
models_dir: models_dir.to_string_lossy().into_owned(),
..Default::default()
};
stub_flux_companion_paths_in_dir(&mut config, models_dir, true);
let mut entry =
flux_unet_only_catalog_entry("1153358", "agfluxSchnell_realistic23.safetensors");
entry.sub_family = Some("flux1-s".into());
let intent = mold_catalog::synthesis::synthesize_intent(&entry, models_dir).unwrap();
let cfg = resolve_intent_to_paths("cv:1153358", &intent, &config).unwrap();
assert_eq!(
cfg.is_schnell,
Some(true),
"flux1-s catalog entries must select FLUX schnell config, not dev guidance config"
);
unsafe {
match _saved {
Some(v) => std::env::set_var("MOLD_HOME", v),
None => std::env::remove_var("MOLD_HOME"),
}
}
}
#[test]
fn resolve_intent_uses_zimage_recipe_text_encoder_and_shared_companion_vae() {
use mold_catalog::entry::{
Bundling, CatalogEntry, CatalogId, DownloadRecipe, FamilyRole, FileFormat,
LicenseFlags, Modality, RecipeFile, RecipeFileRole, Source, TokenKind,
};
use mold_catalog::families::Family;
let dir = tempfile::tempdir().unwrap();
let models_dir = dir.path();
let _saved = std::env::var("MOLD_HOME").ok();
unsafe { std::env::set_var("MOLD_HOME", models_dir.to_string_lossy().as_ref()) };
let mut config = mold_core::Config {
models_dir: models_dir.to_string_lossy().into_owned(),
..Default::default()
};
let te_dir = models_dir.join("z-image-te");
config.models.insert(
"z-image-te".into(),
mold_core::ModelConfig {
family: Some("companion".into()),
transformer: Some(
te_dir
.join("text_encoder/model-00001-of-00003.safetensors")
.to_string_lossy()
.into_owned(),
),
vae: Some(
te_dir
.join("vae/diffusion_pytorch_model.safetensors")
.to_string_lossy()
.into_owned(),
),
text_encoder_files: Some(vec![
te_dir
.join("text_encoder/model-00001-of-00003.safetensors")
.to_string_lossy()
.into_owned(),
te_dir
.join("text_encoder/model-00002-of-00003.safetensors")
.to_string_lossy()
.into_owned(),
te_dir
.join("text_encoder/model-00003-of-00003.safetensors")
.to_string_lossy()
.into_owned(),
]),
text_tokenizer: Some(
te_dir
.join("tokenizer/tokenizer.json")
.to_string_lossy()
.into_owned(),
),
..Default::default()
},
);
let entry = CatalogEntry {
id: CatalogId::from("cv:2442439"),
source: Source::Civitai,
source_id: "2442439".into(),
name: "Z Image Turbo".into(),
author: Some("z".into()),
family: Family::ZImage,
family_role: FamilyRole::Finetune,
sub_family: None,
modality: Modality::Image,
kind: mold_catalog::entry::Kind::Checkpoint,
file_format: FileFormat::Safetensors,
bundling: Bundling::SingleFile,
size_bytes: Some(12_021_353_906),
download_count: 0,
rating: None,
likes: 0,
nsfw: false,
thumbnail_url: None,
description: None,
license: None,
license_flags: LicenseFlags::default(),
tags: vec![],
companions: vec!["z-image-te".into()],
download_recipe: DownloadRecipe {
files: vec![
RecipeFile {
url: "https://civitai.example/model".into(),
dest: "{family}/civitai/2442439/zImageTurbo_turbo.safetensors".into(),
sha256: None,
size_bytes: Some(12_021_353_906),
role: None,
},
RecipeFile {
url: "https://civitai.example/text".into(),
dest: "{family}/civitai/2442439/zImageTurbo_turbo_txt.safetensors".into(),
sha256: None,
size_bytes: Some(8_044_982_048),
role: Some(RecipeFileRole::TextEncoder),
},
],
needs_token: Some(TokenKind::Civitai),
},
engine_phase: 1,
created_at: None,
updated_at: None,
added_at: 0,
trained_words: vec![],
};
let intent = mold_catalog::synthesis::synthesize_intent(&entry, models_dir).unwrap();
std::fs::create_dir_all(intent.primary_recipe_path.parent().unwrap()).unwrap();
std::fs::write(&intent.primary_recipe_path, b"primary").unwrap();
let cfg = resolve_intent_to_paths("cv:2442439", &intent, &config).unwrap();
let recipe_text_encoder =
models_dir.join("cv-2442439/z-image/civitai/2442439/zImageTurbo_turbo_txt.safetensors");
let shared_vae = te_dir.join("vae/diffusion_pytorch_model.safetensors");
assert_eq!(cfg.vae.as_deref(), shared_vae.to_str());
let expected_text_encoder_files = vec![recipe_text_encoder.to_string_lossy().into_owned()];
assert_eq!(
cfg.text_encoder_files.as_deref(),
Some(expected_text_encoder_files.as_slice())
);
unsafe {
match _saved {
Some(v) => std::env::set_var("MOLD_HOME", v),
None => std::env::remove_var("MOLD_HOME"),
}
}
}
#[test]
fn resolve_intent_returns_error_naming_missing_required_companion() {
let dir = tempfile::tempdir().unwrap();
let models_dir = dir.path();
let primary_path = models_dir
.join("cv-994561/flux/civitai/994561/realHornyProV3_realHornyProV3Unet.safetensors");
std::fs::create_dir_all(primary_path.parent().unwrap()).unwrap();
write_safetensors_with_keys(
&primary_path,
&["double_blocks.0.img_attn.proj.weight", "img_in.weight"],
);
let _saved = std::env::var("MOLD_HOME").ok();
unsafe { std::env::set_var("MOLD_HOME", models_dir.to_string_lossy().as_ref()) };
let config = mold_core::Config {
models_dir: models_dir.to_string_lossy().into_owned(),
..Default::default()
};
let entry =
flux_unet_only_catalog_entry("994561", "realHornyProV3_realHornyProV3Unet.safetensors");
let intent = mold_catalog::synthesis::synthesize_intent(&entry, models_dir).unwrap();
let err = resolve_intent_to_paths("cv:994561", &intent, &config).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("t5-v1_1-xxl") || msg.contains("clip-l") || msg.contains("flux-vae"),
"error must name a specific missing companion, got: {msg}"
);
assert!(matches!(err, ResolveError::CompanionConfigMissing { .. }));
unsafe {
match _saved {
Some(v) => std::env::set_var("MOLD_HOME", v),
None => std::env::remove_var("MOLD_HOME"),
}
}
}
#[test]
fn cv_id_resolves_when_files_arrive_after_initial_request() {
let dir = tempfile::tempdir().unwrap();
let models_dir = dir.path();
let _saved = std::env::var("MOLD_HOME").ok();
unsafe { std::env::set_var("MOLD_HOME", models_dir.to_string_lossy().as_ref()) };
let entry =
flux_unet_only_catalog_entry("994561", "realHornyProV3_realHornyProV3Unet.safetensors");
let mut config = mold_core::Config {
models_dir: models_dir.to_string_lossy().into_owned(),
..Default::default()
};
stub_flux_companion_paths_in_dir(&mut config, models_dir, true);
let intent = mold_catalog::synthesis::synthesize_intent(&entry, models_dir).unwrap();
let err = resolve_intent_to_paths("cv:994561", &intent, &config).unwrap_err();
assert!(matches!(err, ResolveError::PrimaryFileMissing { .. }));
let primary_path = models_dir
.join("cv-994561/flux/civitai/994561/realHornyProV3_realHornyProV3Unet.safetensors");
let vae_path = models_dir.join("flux-vae/ae.safetensors");
std::fs::create_dir_all(primary_path.parent().unwrap()).unwrap();
write_safetensors_with_keys(
&primary_path,
&["double_blocks.0.img_attn.proj.weight", "img_in.weight"],
);
let cfg_second = resolve_intent_to_paths("cv:994561", &intent, &config).unwrap();
assert_eq!(cfg_second.transformer.as_deref(), primary_path.to_str());
assert_eq!(cfg_second.vae.as_deref(), vae_path.to_str());
unsafe {
match _saved {
Some(v) => std::env::set_var("MOLD_HOME", v),
None => std::env::remove_var("MOLD_HOME"),
}
}
}
#[test]
fn resolve_intent_rejects_truncated_sidecar_primary() {
let dir = tempfile::tempdir().unwrap();
let models_dir = dir.path();
let _saved = std::env::var("MOLD_HOME").ok();
unsafe { std::env::set_var("MOLD_HOME", models_dir.to_string_lossy().as_ref()) };
let primary_path = models_dir
.join("cv-994561/flux/civitai/994561/realHornyProV3_realHornyProV3Unet.safetensors");
std::fs::create_dir_all(primary_path.parent().unwrap()).unwrap();
write_safetensors_with_keys(
&primary_path,
&["double_blocks.0.img_attn.proj.weight", "img_in.weight"],
);
let entry =
flux_unet_only_catalog_entry("994561", "realHornyProV3_realHornyProV3Unet.safetensors");
let sidecar = mold_catalog::sidecar::sidecar_from_entry(
&entry,
"flux/civitai/994561/realHornyProV3_realHornyProV3Unet.safetensors".into(),
);
let mut sidecar = sidecar;
sidecar.size_bytes = Some(primary_path.metadata().unwrap().len() + 1);
let sidecar_path = mold_catalog::sidecar::civitai_sidecar_path(models_dir, "cv:994561");
mold_catalog::sidecar::write_sidecar(&sidecar_path, &sidecar).unwrap();
let mut config = mold_core::Config {
models_dir: models_dir.to_string_lossy().into_owned(),
..Default::default()
};
stub_flux_companion_paths_in_dir(&mut config, models_dir, true);
let intent = mold_catalog::synthesis::synthesize_intent(&entry, models_dir).unwrap();
let err = resolve_intent_to_paths("cv:994561", &intent, &config).unwrap_err();
assert!(matches!(err, ResolveError::PrimaryFileMissing { .. }));
unsafe {
match _saved {
Some(v) => std::env::set_var("MOLD_HOME", v),
None => std::env::remove_var("MOLD_HOME"),
}
}
}
#[test]
fn live_error_to_install_error_maps_404_to_not_found() {
let upstream = mold_catalog::live::LiveSearchError::Upstream {
host: "civitai.com",
status: 404,
body: "{\"error\": \"not found\"}".to_string(),
};
let mapped = live_error_to_install_error("cv:42", &upstream);
assert!(matches!(mapped, mold_core::InstallError::NotFound(_)));
}
#[test]
fn live_error_to_install_error_maps_5xx_to_recipe_malformed() {
let upstream = mold_catalog::live::LiveSearchError::Upstream {
host: "civitai.com",
status: 500,
body: "internal".into(),
};
let mapped = live_error_to_install_error("cv:42", &upstream);
assert!(matches!(
mapped,
mold_core::InstallError::RecipeMalformed(_)
));
}
#[test]
fn install_error_to_api_error_maps_network_to_502() {
let err = mold_core::InstallError::Network("dns: civitai.com".into());
let api = install_error_to_api_error(&err);
assert!(api.error.contains("network unreachable"));
}
#[test]
fn install_error_to_api_error_maps_not_found_to_404() {
let err = mold_core::InstallError::NotFound("cv:99999999".into());
let api = install_error_to_api_error(&err);
assert_eq!(api.code, "MODEL_NOT_FOUND");
}
const BUDGET_FRACTION_NUMERATOR: u64 = 9;
const BUDGET_FRACTION_DENOMINATOR: u64 = 10;
fn expected_budget_cap(available: u64) -> u64 {
available * BUDGET_FRACTION_NUMERATOR / BUDGET_FRACTION_DENOMINATOR
}
#[test]
fn preflight_error_message_states_correct_budget_cap() {
let peak: u64 = 24_400_000_000;
let available: u64 = 25_300_000_000;
let cap = expected_budget_cap(available);
assert!(
peak > cap,
"test invariant: peak ({peak}) must exceed cap ({cap})"
);
let result = check_model_memory_budget(
"qwen-image:q8",
peak,
available,
"Try a smaller variant (e.g. ':q5' / ':q4'), enable --offload (FLUX), or close other GPU apps.",
);
assert!(result.is_err(), "expected rejection, got Ok");
let err = result.unwrap_err();
let msg = &err.error;
let cap_gb = cap as f64 / 1_000_000_000.0;
let cap_str = format!("{cap_gb:.1}");
assert!(
msg.contains("budget cap"),
"error must mention 'budget cap', got: {msg}"
);
assert!(
msg.contains(&cap_str),
"error must contain the cap value ~{cap_str} GB, got: {msg}"
);
let available_gb = available as f64 / 1_000_000_000.0;
let available_str = format!("{available_gb:.1}");
let _ = available_str; }
#[test]
fn preflight_error_message_does_not_imply_peak_less_than_available() {
let scenarios: &[(f64, f64)] = &[
(24.4, 25.3), (19.0, 20.0), (10.0, 10.5), (30.0, 32.0), (9.1, 10.0), ];
for &(peak_gb, available_gb) in scenarios {
let peak = (peak_gb * 1_000_000_000.0) as u64;
let available = (available_gb * 1_000_000_000.0) as u64;
let cap = expected_budget_cap(available);
if peak <= cap {
continue;
}
let result =
check_model_memory_budget("test-model", peak, available, "Try a smaller variant.");
assert!(
result.is_err(),
"expected rejection for peak={peak_gb} available={available_gb}, got Ok"
);
let msg = result.unwrap_err().error;
let cap_gb = cap as f64 / 1_000_000_000.0;
let cap_str = format!("{cap_gb:.1}");
assert!(
msg.contains("budget cap"),
"scenario peak={peak_gb} available={available_gb}: \
message must say 'budget cap', got: {msg}"
);
assert!(
msg.contains(&cap_str),
"scenario peak={peak_gb} available={available_gb}: \
message must include cap={cap_str}, got: {msg}"
);
}
}
fn ltx_video_13b_paths(
transformer_gb: u64,
vae_gb: u64,
t5_gb: u64,
) -> (tempfile::TempDir, ModelPaths) {
let dir = tempfile::tempdir().expect("tempdir");
let mk = |name: &str, sz: u64| {
let p = dir.path().join(name);
let f = std::fs::File::create(&p).unwrap();
f.set_len(sz * GB).unwrap();
p
};
let transformer = mk("ltx-video-0.9.8-13b-dev_fp16.safetensors", transformer_gb);
let vae = mk("ltx-video-vae.safetensors", vae_gb);
let t5 = mk("t5xxl_fp16.safetensors", t5_gb);
let paths = ModelPaths {
transformer,
transformer_shards: Vec::new(),
vae,
spatial_upscaler: None,
temporal_upscaler: None,
distilled_lora: None,
t5_encoder: Some(t5),
clip_encoder: None,
t5_tokenizer: None,
clip_tokenizer: None,
clip_encoder_2: None,
clip_tokenizer_2: None,
text_encoder_files: Vec::new(),
text_tokenizer: None,
decoder: None,
};
(dir, paths)
}
#[test]
fn preflight_rejects_ltx_video_13b_at_768x512x25_on_24gb_card() {
let (_dir, paths) = ltx_video_13b_paths(26, 1, 10);
let hint = ActivationHint {
width: 768,
height: 512,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::LtxVideo,
};
let result = preflight_memory_guard_with_available(
"ltx-video-0.9.8-13b-dev:bf16",
&paths,
0,
24 * GB,
Some(hint),
);
assert!(
result.is_err(),
"13B LTX-Video BF16 (26 GB transformer) must be rejected on a 24 GB card — \
the transformer is not streamed and its full weight must be counted, \
got {result:?}",
);
let err = result.unwrap_err();
assert!(
err.error.contains("frames") || err.error.contains("width"),
"rejection message must suggest reducing frames or resolution, got: {}",
err.error,
);
}
#[test]
fn preflight_estimate_for_ltx_video_13b_within_expected_range() {
let (_dir, paths) = ltx_video_13b_paths(26, 1, 10);
let expected_gb = 29u64;
let peak = mold_inference::device::estimate_peak_memory(
&paths,
mold_inference::LoadStrategy::Sequential,
);
let peak_gb = peak / GB;
assert!(
peak_gb >= expected_gb.saturating_sub(3),
"peak estimate ({peak_gb} GB) is unexpectedly low — LTX-Video 13B BF16 \
sequential estimate should be ≥ {} GB",
expected_gb.saturating_sub(3),
);
assert!(
peak_gb <= expected_gb + 3,
"peak estimate ({peak_gb} GB) is unexpectedly high for 26+1+10 GB layout \
— should be ≤ {} GB",
expected_gb + 3,
);
}
#[test]
fn activation_family_for_ltx_video_is_non_streaming() {
let family = mold_inference::device::activation_family_for("ltx-video");
assert_eq!(
family,
ActivationFamily::LtxVideo,
"ltx-video slug must map to LtxVideo (non-streaming, full-weight load)"
);
assert!(
!family.streaming_transformer(),
"LtxVideo must NOT be treated as a streaming transformer — \
it loads the entire weight file into VRAM at generate time"
);
assert!(
mold_inference::device::activation_family_for("ltx2").streaming_transformer(),
"ltx2 must still map to the streaming family"
);
}
#[test]
fn preflight_rejection_message_for_ltx_video_suggests_frames_or_resolution() {
let (_dir, paths) = ltx_video_13b_paths(26, 1, 10);
let hint = ActivationHint {
width: 768,
height: 512,
batch: 1,
dtype_bytes: 2,
family: ActivationFamily::LtxVideo,
};
let result = preflight_memory_guard_with_available(
"ltx-video-0.9.8-13b-dev:bf16",
&paths,
0,
24 * GB,
Some(hint),
);
let err = result.expect_err("must reject");
assert!(
err.error.contains("frames") || err.error.contains("width"),
"LTX-Video rejection message must suggest reducing frames or \
width/height (not --offload), got: {}",
err.error,
);
assert!(
!err.error.contains("--offload"),
"LTX-Video rejection must not mention --offload (FLUX-only flag), \
got: {}",
err.error,
);
}
#[test]
fn preflight_rejection_message_for_image_suggests_resolution_not_frames() {
let hint = ActivationHint {
width: 1024,
height: 1024,
batch: 2,
dtype_bytes: 2,
family: ActivationFamily::SdxlUnet,
};
let suggestion = rejection_suggestion(Some(hint));
assert!(
suggestion.contains("--width/--height"),
"image preflight suggestion should mention resolution; got: {suggestion}"
);
assert!(
suggestion.contains("--batch"),
"image preflight suggestion should mention batch; got: {suggestion}"
);
assert!(
!suggestion.contains("--frames"),
"image preflight suggestion must not mention video frames; got: {suggestion}"
);
}
}