use std::path::{Path, PathBuf};
use std::time::SystemTime;
use anyhow::{Context, bail};
use super::CompareArgs;
use super::export::{fail, holds_on_target, upload_stage};
use super::front::Frontend;
use super::train::{POLL, executor, prepare};
use crate::compare::{
COMPARE_PREFIX, COMPARES_DIR, ChildSettings, CompareJob, CompareSetup, EvalQuestion, JudgeInfo,
JudgeRun, ModelSource, Parts, SCRIPT_FILE, build_report, find_compare, judge_all, judge_key,
latest_export, newest_exported_run, prices, read_answers, read_eval, read_hardware,
verdicts_file, write_report,
};
use crate::config::{Settings, Source, Target};
use crate::dataset::DataFiles;
use crate::exec::{Executor, JobRuntime, quote};
use crate::export::{ExportRecord, link_file};
use crate::llm::RetryPolicy;
use crate::prompts::{JUDGE, Prompts};
use crate::runpod::{GpuOffer, PodRecord, RunpodClient, RunpodTarget, cheapest_in_stock};
use crate::runs::{
Launch, Outcome, RunCtx, RunRecord, RunState, Runs, cancel, create, rfc3339, start_mounted,
watch,
};
use crate::train::CONFIG_FILE;
#[derive(Debug, Clone)]
pub(crate) struct Plan {
pub(crate) run: RunRecord,
pub(crate) gguf: PathBuf,
pub(crate) export: ExportRecord,
pub(crate) questions: Vec<EvalQuestion>,
pub(crate) context: u32,
pub(crate) base_model: Option<String>,
}
pub(crate) fn plan(
runs: &Runs,
project_dir: &Path,
run: Option<&str>,
limit: Option<u32>,
) -> anyhow::Result<Plan> {
let record = match run {
Some(id) => runs.load(id)?,
None => newest_exported_run(runs)?.context(
"no run has a GGUF: export one with `overbrainer export <run-id>`, then compare",
)?,
};
let export = latest_export(runs, &record.id).with_context(|| {
format!(
"run {id} has no GGUF; run `overbrainer export {id}` first",
id = record.id
)
})?;
let run_dir = runs.run_dir(&record.id)?;
let limit = limit.map(|limit| usize::try_from(limit).unwrap_or(usize::MAX));
let questions = read_eval(&DataFiles::new(project_dir).eval, limit)?;
let config = run_dir.join(CONFIG_FILE);
Ok(Plan {
gguf: run_dir.join(&export.file),
context: crate::export::sequence_len(&config).unwrap_or(0),
base_model: std::fs::read_to_string(&config)
.ok()
.and_then(|text| crate::train::top_level_scalar(&text, "base_model")),
run: record,
export,
questions,
})
}
pub(super) fn setup_of(plan: &Plan, compare: &str, seed: u64) -> CompareSetup {
CompareSetup {
run: plan.run.id.clone(),
compare: compare.to_string(),
gguf: plan.export.file.clone(),
gguf_sha256: plan.export.sha256.clone(),
quantize: plan.export.quantize.clone(),
llama_cpp: crate::export::LLAMA_CPP_TAG.to_string(),
base_model: plan.base_model.clone(),
seed,
created: rfc3339(SystemTime::now()),
questions: plan.questions.clone(),
}
}
pub(super) fn job_of(
plan: &Plan,
settings: &Settings,
model: ModelSource,
) -> anyhow::Result<CompareJob> {
Ok(CompareJob::new(
model,
plan.questions.clone(),
ChildSettings::from_config(&settings.compare, plan.context),
)?)
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum PodPrice {
InStock(GpuOffer),
Cap(f64),
Unknown,
}
impl PodPrice {
pub(crate) fn words(&self) -> String {
match self {
Self::InStock(offer) => format!(
"at about ${:.2}/h ({}, the cheapest GPU type in stock)",
offer.per_hour, offer.gpu_type
),
Self::Cap(cap) => format!(
"at most ${cap:.2}/h (max_price_per_hour: the catalog shows no price in stock)"
),
Self::Unknown => "at a price the catalog does not show (no GPU type in stock with \
a price, and no max_price_per_hour)"
.to_string(),
}
}
}
pub(crate) async fn pod_price(
client: &RunpodClient,
spec: &RunpodTarget,
floor_gb: Option<u32>,
) -> PodPrice {
match client.list_gpu_types(spec.gpu_count).await {
Ok(gpus) => {
if let Some(offer) = cheapest_in_stock(spec, &gpus, floor_gb) {
return PodPrice::InStock(offer);
}
},
Err(error) => tracing::debug!("cannot read the GPU catalog for the price: {error}"),
}
spec.max_price_per_hour
.map_or(PodPrice::Unknown, PodPrice::Cap)
}
pub(crate) fn compare_vram_floor(spec: &RunpodTarget, bytes: u64) -> Option<u32> {
(spec.gpu_types.is_auto() && spec.min_vram_gb.is_none())
.then(|| super::runpod_train::gguf_vram_floor(bytes))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Preview {
pub(crate) run: String,
pub(crate) gguf: String,
pub(crate) questions: usize,
pub(crate) target: String,
pub(crate) runpod: Option<String>,
}
pub(crate) async fn preview(
project_dir: &Path,
source: &Source,
run: Option<&str>,
) -> anyhow::Result<Preview> {
let (dir, source, run) = (
project_dir.to_path_buf(),
source.clone(),
run.map(str::to_string),
);
let (settings, plan) = tokio::task::spawn_blocking(move || -> anyhow::Result<_> {
let settings = source.load(&dir)?;
let plan = plan(&Runs::new(&dir), &dir, run.as_deref(), None)?;
Ok((settings, plan))
})
.await
.context("cannot read the run to compare")??;
let target = target_of(&settings, &plan.run)?;
let runpod = match RunpodTarget::from_target(target) {
Some(spec) => {
let client = super::pod::client(&settings).await;
Some(runpod_line(client, &spec, plan.export.size).await)
},
None => None,
};
Ok(Preview {
run: plan.run.id.clone(),
gguf: plan.export.file.clone(),
questions: plan.questions.len(),
target: plan.run.target.clone(),
runpod,
})
}
async fn runpod_line(
client: anyhow::Result<RunpodClient>,
spec: &RunpodTarget,
gguf_bytes: u64,
) -> String {
let price = match client {
Ok(client) => pod_price(&client, spec, compare_vram_floor(spec, gguf_bytes)).await,
Err(error) => {
tracing::debug!("cannot price the compare's pod: {error:#}");
spec.max_price_per_hour
.map_or(PodPrice::Unknown, PodPrice::Cap)
},
};
match price {
PodPrice::InStock(_) => format!("a new Runpod pod, {}", price.words()),
PodPrice::Cap(cap) => format!("a new Runpod pod, at most ${cap:.2}/h"),
PodPrice::Unknown => "a new Runpod pod, at the price picked when it starts".to_string(),
}
}
fn target_of<'a>(settings: &'a Settings, run: &RunRecord) -> anyhow::Result<&'a Target> {
settings.targets.get(&run.target).with_context(|| {
format!(
"target `{}` of run {} is no longer in overbrainer.toml",
run.target, run.id
)
})
}
pub(crate) async fn run(
project_dir: &Path,
args: &CompareArgs,
front: &Frontend,
source: &Source,
) -> anyhow::Result<()> {
let settings = source.load(project_dir)?;
let runs = Runs::new(project_dir);
if let Some(id) = &args.rejudge {
let compares = find_compare(&runs, args.run.as_deref(), id)?;
let judging = Judging {
project_dir,
settings: &settings,
compares: &compares,
id,
front,
};
let report = judge_and_report(&judging).await?;
front.line(&report.to_string_lossy());
return Ok(());
}
let plan = plan(&runs, project_dir, args.run.as_deref(), args.limit)?;
let target = target_of(&settings, &plan.run)?;
let spec = RunpodTarget::from_target(target);
if spec.is_none() && args.keep_pod {
bail!("--keep-pod only applies to a runpod target");
}
tracing::info!(
"compare: run {} ({}) on {} questions, on target `{}`",
plan.run.id,
plan.export.quantize,
plan.questions.len(),
plan.run.target
);
let (compares, id) = if let Some(spec) = spec {
Box::pin(super::runpod_train::compare(
project_dir,
&settings,
(&spec, args.keep_pod),
&plan,
front,
))
.await?
} else {
Box::pin(on_target(project_dir, &settings, target, &plan, front)).await?
};
let judging = Judging {
project_dir,
settings: &settings,
compares: &compares,
id: &id,
front,
};
let report = judge_and_report(&judging).await?;
front.line(&report.to_string_lossy());
Ok(())
}
async fn on_target(
project_dir: &Path,
settings: &Settings,
target: &Target,
plan: &Plan,
front: &Frontend,
) -> anyhow::Result<(Runs, String)> {
let run = &plan.run;
let runtime = JobRuntime::from_target(target)
.with_context(|| format!("target `{}` cannot run a compare", run.target))?;
let model = format!("{}/{}", runtime.root(&run.remote_dir), plan.export.file);
let job = job_of(plan, settings, ModelSource::OnTarget(model))?;
let mut interrupt = front.interrupt();
let executor = prepare(&mut interrupt, executor(project_dir, &run.target, target)).await?;
let runs = Runs::new(project_dir);
let compares = runs.compares(&run.id)?;
let jobs = format!("{}/{COMPARES_DIR}", run.remote_dir);
let record = create(&compares, COMPARE_PREFIX, &jobs, &run.target)?;
let id = record.id.clone();
let job = job.with_script(format!(
"{}/{COMPARES_DIR}/{id}/{SCRIPT_FILE}",
runtime.root(&run.remote_dir)
));
let job_dir = compares.run_dir(&id)?;
tracing::info!("compare: {id}: {}", compares.relative_dir(&id));
let guard = front.open_bus();
let ctx = RunCtx {
runs: &compares,
executor: &executor,
bus: &guard.bus,
poll: POLL,
};
let launch = Launch {
runtime: &runtime,
secrets: Vec::new(),
};
let started = interrupt
.shield(async {
setup_of(plan, &id, settings.pipeline.seed).save(&job_dir)?;
let stage = job_dir.join(".upload");
gguf_on_target(&executor, plan, &runs.run_dir(&run.id)?, &stage).await?;
Ok(start_mounted(&ctx, &job, launch, record, &run.remote_dir).await?)
})
.await
.inspect_err(|error| fail(&compares, &id, error));
let result = match started {
Err(error) => Err(error),
Ok(record) => {
if let Some(outcome) = interrupt.race(watch(&ctx, &job, record.clone())).await {
outcome.map_err(|error| {
anyhow::Error::from(error).context(format!(
"compare {id} of run {} may still run on target `{}`; {}",
run.id,
run.target,
compares.follow_hint(&id)
))
})
} else {
interrupt
.shield(cancel(&compares, &executor, &job, record))
.await?;
Err(anyhow::anyhow!(
"interrupted: compare {id} of run {} cancelled",
run.id
))
}
},
};
guard.close().await;
generated(&compares, &result?)?;
Ok((compares, id))
}
async fn gguf_on_target<E: Executor>(
executor: &E,
plan: &Plan,
local_run: &Path,
stage: &Path,
) -> anyhow::Result<()> {
let remote = &plan.run.remote_dir;
let file = &plan.export.file;
let path = quote(&format!("{remote}/{file}"));
if holds_on_target(executor, &format!("[ -f {path} ]")).await? {
let digests = executor
.manifest(remote, std::slice::from_ref(file), &[])
.await?;
if digests
.iter()
.any(|digest| digest.path == *file && digest.sha256 == plan.export.sha256)
{
return Ok(());
}
}
tracing::info!(
"compare: {remote} does not hold the GGUF of run {} as exported: uploading it",
plan.run.id
);
link_file(&local_run.join(file), &stage.join(file))?;
upload_stage(executor, stage, remote).await
}
pub(super) fn generated(compares: &Runs, outcome: &Outcome) -> anyhow::Result<()> {
let record = &outcome.record;
let subject = compares.subject(&record.id);
match record.state {
RunState::Succeeded => Ok(()),
RunState::Cancelled => bail!("{subject} was cancelled"),
_ => {
let message = record.message.as_deref().unwrap_or("it failed");
bail!(
"{subject} failed: {message}; its server log is {}/{}",
compares.relative_dir(&record.id),
crate::compare::SERVER_LOG
)
},
}
}
pub(super) struct Judging<'a> {
pub(super) project_dir: &'a Path,
pub(super) settings: &'a Settings,
pub(super) compares: &'a Runs,
pub(super) id: &'a str,
pub(super) front: &'a Frontend,
}
pub(super) async fn judge_and_report(judging: &Judging<'_>) -> anyhow::Result<PathBuf> {
let Judging {
project_dir,
settings,
compares,
id,
front,
} = judging;
let dir = compares.run_dir(id)?;
let setup = CompareSetup::load(&dir)?;
let answers = read_answers(&dir)?;
if answers.is_empty() {
bail!(
"compare {id} has no answers of the child: run `overbrainer compare --run {}` again",
setup.run
);
}
let prompts = Prompts::load(project_dir)?;
let role = settings.roles.judge_model();
let key = judge_key(role, &prompts.source(JUDGE).unwrap_or_default());
let file_name = verdicts_file(&key);
let file = dir.join(&file_name);
let client = crate::llm::connect(settings, role, &super::resolver()).await?;
let guard = front.open_bus();
let run = JudgeRun {
client: &client,
role,
prompts: &prompts,
policy: RetryPolicy::new(settings.pipeline.max_retries),
concurrency: settings.pipeline.concurrency,
bus: &guard.bus,
compare: id,
};
tracing::info!(
"compare: {id}: judging with {}/{}",
role.provider,
role.model
);
let mut interrupt = front.interrupt();
let judged = interrupt
.race(judge_all(&run, &setup, &answers, &file))
.await;
guard.close().await;
let Some(verdicts) = judged else {
bail!(
"interrupted: the verdicts so far are kept; resume with `overbrainer compare \
--rejudge {id}`"
);
};
let verdicts = verdicts.with_context(|| {
format!("the verdicts so far are kept; resume with `overbrainer compare --rejudge {id}`")
})?;
let pod_price = PodRecord::load(compares, id)?.and_then(|pod| pod.cost_per_hour);
let (prices, child_price_from_pod) = prices(&settings.compare, pod_price);
let report = build_report(&Parts {
setup: &setup,
answers: &answers,
verdicts: &verdicts,
hardware: read_hardware(&dir),
judge: JudgeInfo {
role,
is_parent: settings.roles.judge.is_none(),
verdicts_file: &file_name,
},
prices,
child_price_from_pod,
});
Ok(write_report(&dir, &report)?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::ListOrAuto;
fn spec(gpu_types: ListOrAuto, cap: Option<f64>) -> RunpodTarget {
RunpodTarget {
gpu_types,
min_vram_gb: None,
max_price_per_hour: cap,
gpu_count: 1,
image: "img".into(),
venv: Some("/venv".into()),
container_disk_gb: 50,
max_hours: 1.0,
max_cost_usd: None,
boot_grace: std::time::Duration::from_mins(30),
retrieve_grace: std::time::Duration::from_secs(3600),
data_center_ids: ListOrAuto::default(),
network_volume_id: None,
max_volume_gb: None,
ssh_client: crate::config::SshClient::Openssh,
}
}
const GGUF: &str = "output/gguf/r1-Q4_K_M.gguf";
fn plan_of(remote_dir: &Path, sha256: String) -> Plan {
Plan {
run: RunRecord {
id: "r1".into(),
target: "box".into(),
created: "2026-10-06T10:00:00Z".into(),
remote_dir: remote_dir.to_string_lossy().into_owned(),
job: None,
state: RunState::Succeeded,
message: None,
snapshot: None,
resumed_from: None,
snapshots: true,
},
gguf: PathBuf::from(GGUF),
export: ExportRecord {
quantize: "Q4_K_M".into(),
llama_cpp: crate::export::LLAMA_CPP_TAG.into(),
file: GGUF.into(),
sha256,
size: 4,
created: "2026-10-06T11:00:00Z".into(),
},
questions: Vec::new(),
context: 0,
base_model: None,
}
}
#[tokio::test]
async fn the_gguf_is_uploaded_only_where_it_is_missing_or_different()
-> Result<(), Box<dyn std::error::Error>> {
let local = tempfile::tempdir()?;
let target = tempfile::tempdir()?;
let executor = crate::exec::LocalExecutor::new(target.path())?;
let remote = target.path().join("r1");
let gguf = local.path().join(GGUF);
std::fs::create_dir_all(gguf.parent().ok_or("no gguf dir")?)?;
std::fs::write(&gguf, "gguf")?;
let plan = plan_of(&remote, crate::exec::sha256_file(&gguf)?);
let stage = local.path().join("compares/c1/.upload");
gguf_on_target(&executor, &plan, local.path(), &stage).await?;
let uploaded = remote.join(&plan.export.file);
assert_eq!(std::fs::read_to_string(&uploaded)?, "gguf");
assert!(!stage.exists(), "the stage is removed");
std::fs::write(&uploaded, "stale")?;
gguf_on_target(&executor, &plan, local.path(), &stage).await?;
assert_eq!(std::fs::read_to_string(&uploaded)?, "gguf");
std::fs::remove_file(&gguf)?;
gguf_on_target(&executor, &plan, local.path(), &stage).await?;
assert!(!stage.exists());
Ok(())
}
#[tokio::test]
async fn the_pod_price_is_the_cheapest_in_stock_else_the_cap()
-> Result<(), Box<dyn std::error::Error>> {
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v2/catalog/gpus"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({"gpus": [
{"id": "NVIDIA A40", "memory": 48, "secure": true,
"price": {"secure": 0.4}, "maxCount": {"secure": 8},
"availability": "HIGH"},
{"id": "NVIDIA H100", "memory": 80, "secure": true,
"price": {"secure": 2.5}, "maxCount": {"secure": 8},
"availability": "NONE"}
]})),
)
.mount(&server)
.await;
let client = RunpodClient::new(
&format!("{}/v2", server.uri()),
&secrecy::SecretString::from("k"),
)?;
let auto = pod_price(&client, &spec(ListOrAuto::Auto, None), None).await;
assert_eq!(
auto.words(),
"at about $0.40/h (NVIDIA A40, the cheapest GPU type in stock)"
);
let sold_out = spec(ListOrAuto::List(vec!["NVIDIA H100".into()]), Some(3.0));
let capped = pod_price(&client, &sold_out, None).await;
assert_eq!(capped, PodPrice::Cap(3.0));
assert!(
capped.words().starts_with("at most $3.00/h"),
"{}",
capped.words()
);
let unknown = spec(ListOrAuto::List(vec!["NVIDIA H100".into()]), None);
assert_eq!(pod_price(&client, &unknown, None).await, PodPrice::Unknown);
let line = runpod_line(Ok(client), &spec(ListOrAuto::Auto, None), 4).await;
assert_eq!(
line,
"a new Runpod pod, at about $0.40/h (NVIDIA A40, the cheapest GPU type in stock)"
);
Ok(())
}
#[tokio::test]
async fn the_preview_falls_back_to_the_cap() -> Result<(), Box<dyn std::error::Error>> {
use wiremock::matchers::method;
use wiremock::{Mock, MockServer, ResponseTemplate};
let server = MockServer::start().await;
Mock::given(method("GET"))
.respond_with(ResponseTemplate::new(500))
.mount(&server)
.await;
let client = RunpodClient::new(
&format!("{}/v2", server.uri()),
&secrecy::SecretString::from("k"),
)?;
let capped = spec(ListOrAuto::Auto, Some(0.5));
assert_eq!(
runpod_line(Ok(client), &capped, 4).await,
"a new Runpod pod, at most $0.50/h"
);
let no_client = || Err(anyhow::anyhow!("no Runpod API key"));
assert_eq!(
runpod_line(no_client(), &capped, 4).await,
"a new Runpod pod, at most $0.50/h"
);
assert_eq!(
runpod_line(no_client(), &spec(ListOrAuto::Auto, None), 4).await,
"a new Runpod pod, at the price picked when it starts"
);
Ok(())
}
}