use std::time::Duration;
use crate::config::{DEFAULT_RUNPOD_IMAGE, DEFAULT_RUNPOD_VENV, ListOrAuto, SshClient, Target};
use crate::exec::JobRuntime;
use super::JOB_ENV;
pub const WORKDIR: &str = "/workspace/overbrainer";
pub const VOLUME_MOUNT: &str = "/workspace/data";
pub const VOLUME_WORKDIR: &str = "/workspace/data/overbrainer";
pub const MIN_CUDA_VERSION: &str = "13.0";
#[derive(Debug, Clone, PartialEq)]
pub struct RunpodTarget {
pub gpu_types: ListOrAuto,
pub min_vram_gb: Option<u32>,
pub max_price_per_hour: Option<f64>,
pub gpu_count: u32,
pub image: String,
pub venv: Option<String>,
pub container_disk_gb: u32,
pub max_hours: f64,
pub max_cost_usd: Option<f64>,
pub boot_grace: Duration,
pub retrieve_grace: Duration,
pub data_center_ids: ListOrAuto,
pub network_volume_id: Option<String>,
pub max_volume_gb: Option<u32>,
pub ssh_client: SshClient,
}
impl RunpodTarget {
#[must_use]
pub fn from_target(target: &Target) -> Option<Self> {
let Target::Runpod {
gpu_types,
min_vram_gb,
max_price_per_hour,
gpu_count,
image,
venv,
container_disk_gb,
max_hours,
max_cost_usd,
boot_grace_minutes,
retrieve_grace_minutes,
data_center_ids,
network_volume_id,
max_volume_gb,
ssh_client,
} = target
else {
return None;
};
Some(Self {
gpu_types: gpu_types.clone(),
min_vram_gb: *min_vram_gb,
max_price_per_hour: *max_price_per_hour,
gpu_count: *gpu_count,
image: image
.clone()
.unwrap_or_else(|| DEFAULT_RUNPOD_IMAGE.to_string()),
venv: Some(
venv.clone()
.unwrap_or_else(|| DEFAULT_RUNPOD_VENV.to_string()),
),
container_disk_gb: *container_disk_gb,
max_hours: *max_hours,
max_cost_usd: *max_cost_usd,
boot_grace: Duration::from_secs(u64::from(*boot_grace_minutes) * 60),
retrieve_grace: Duration::from_secs(u64::from(*retrieve_grace_minutes) * 60),
data_center_ids: data_center_ids.clone(),
network_volume_id: network_volume_id.clone(),
max_volume_gb: *max_volume_gb,
ssh_client: *ssh_client,
})
}
#[must_use]
pub fn workdir(&self) -> &'static str {
if self.network_volume_id.is_some() {
VOLUME_WORKDIR
} else {
WORKDIR
}
}
#[must_use]
pub fn runtime(&self) -> JobRuntime {
JobRuntime::Native {
venv: self.venv.clone(),
env_file: Some(JOB_ENV.to_string()),
}
}
#[must_use]
pub fn for_compare(&self, image: Option<&str>) -> Self {
Self {
image: image.map_or_else(|| self.image.clone(), str::to_string),
venv: None,
..self.clone()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn config_target(volume: Option<&str>) -> Target {
Target::Runpod {
gpu_types: ListOrAuto::List(vec!["NVIDIA A40".into()]),
min_vram_gb: None,
max_price_per_hour: None,
gpu_count: 1,
image: None,
venv: None,
container_disk_gb: 50,
max_hours: 6.0,
max_cost_usd: None,
boot_grace_minutes: 30,
retrieve_grace_minutes: 60,
data_center_ids: ListOrAuto::List(vec!["EU-RO-1".into()]),
network_volume_id: volume.map(str::to_string),
max_volume_gb: None,
ssh_client: crate::config::SshClient::Openssh,
}
}
#[test]
fn defaults_are_applied() -> Result<(), &'static str> {
let target = RunpodTarget::from_target(&config_target(None)).ok_or("not runpod")?;
assert_eq!(target.image, DEFAULT_RUNPOD_IMAGE);
assert_eq!(target.venv.as_deref(), Some("/workspace/axolotl-venv"));
assert_eq!(target.boot_grace, Duration::from_mins(30));
assert_eq!(target.retrieve_grace, Duration::from_secs(3600));
assert_eq!(target.workdir(), "/workspace/overbrainer");
assert_eq!(
target.runtime(),
JobRuntime::Native {
venv: Some("/workspace/axolotl-venv".into()),
env_file: Some("/etc/overbrainer/job.env".into()),
}
);
let on_volume =
RunpodTarget::from_target(&config_target(Some("vol1"))).ok_or("not runpod")?;
assert_eq!(on_volume.workdir(), "/workspace/data/overbrainer");
Ok(())
}
#[test]
fn a_compare_pod_runs_the_target_image_without_venv() -> Result<(), &'static str> {
let target = RunpodTarget::from_target(&config_target(None)).ok_or("not runpod")?;
let compare = target.for_compare(None);
assert_eq!(compare.image, DEFAULT_RUNPOD_IMAGE);
assert_eq!(
compare.runtime(),
JobRuntime::Native {
venv: None,
env_file: Some("/etc/overbrainer/job.env".into()),
}
);
assert_eq!(compare.gpu_types, target.gpu_types);
assert_eq!(compare.workdir(), target.workdir());
let custom = target.for_compare(Some("me/llama:1@sha256:abc"));
assert_eq!(custom.image, "me/llama:1@sha256:abc");
let mut own = config_target(None);
if let Target::Runpod { image, .. } = &mut own {
*image = Some("me/axolotl:1@sha256:def".into());
}
let own = RunpodTarget::from_target(&own).ok_or("not runpod")?;
assert_eq!(own.for_compare(None).image, "me/axolotl:1@sha256:def");
assert_eq!(
target.runtime(),
JobRuntime::Native {
venv: Some("/workspace/axolotl-venv".into()),
env_file: Some("/etc/overbrainer/job.env".into()),
},
"the training target keeps its venv"
);
Ok(())
}
}