use crate::cluster::resolve_local_hostname;
use crate::config::{
ClusterConfig, ClusterController, ClusterWorker, DEFAULT_CONTROLLER_PORT, LocalDevices,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GpusSpec {
All,
List(Vec<u8>),
}
impl GpusSpec {
pub fn parse(raw: &str) -> Result<Self, String> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err("--gpus requires a value (e.g. `--gpus 0,1` or `--gpus all`)".to_string());
}
if trimmed.eq_ignore_ascii_case("all") {
return Ok(GpusSpec::All);
}
let mut out = Vec::new();
for part in trimmed.split(',') {
let p = part.trim();
if p.is_empty() {
return Err(format!("--gpus: empty entry in {trimmed:?}"));
}
let idx: u8 = p
.parse()
.map_err(|e| format!("--gpus: cannot parse {p:?} as device index: {e}"))?;
out.push(idx);
}
let mut sorted = out.clone();
sorted.sort_unstable();
for win in sorted.windows(2) {
if win[0] == win[1] {
return Err(format!(
"--gpus: duplicate device index {} in {trimmed:?}",
win[0]
));
}
}
Ok(GpusSpec::List(out))
}
pub fn resolve(&self) -> Result<Vec<u8>, String> {
match self {
GpusSpec::List(v) => Ok(v.clone()),
GpusSpec::All => {
let devices = local_gpu_count().map_err(|e| format!("--gpus all: {e}"))?;
if devices > u8::MAX as usize {
return Err(format!(
"--gpus all: {devices} GPUs detected, which exceeds \
the supported device-index range (0..255). Specify \
devices explicitly via --gpus."
));
}
Ok((0u8..devices as u8).collect())
}
}
}
}
pub fn local_gpu_count() -> Result<usize, String> {
flodl_hw::survey().require_devices().map(|d| d.len())
}
pub fn synthesize_local_cluster(devices: &[u8]) -> Result<ClusterConfig, String> {
if devices.is_empty() {
return Err("synthesize_local_cluster: device list is empty".to_string());
}
let hostname = resolve_local_hostname();
let path = std::env::current_dir()
.map(|p| p.to_string_lossy().into_owned())
.map_err(|e| format!("synthesize_local_cluster: cannot read current_dir: {e}"))?;
let port = std::env::var("FLODL_CONTROLLER_PORT")
.ok()
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(DEFAULT_CONTROLLER_PORT);
Ok(ClusterConfig {
controller: ClusterController {
host: "127.0.0.1".to_string(),
port,
path: path.clone(),
docker: None,
arch: None,
data_path: None,
join: None,
},
workers: vec![ClusterWorker {
host: hostname,
ranks: (0..devices.len()).collect(),
local_devices: LocalDevices::Explicit(devices.to_vec()),
nccl_socket_ifname: "lo".to_string(),
path,
ssh: None,
tunnel: false,
arch: None,
data_path: None,
gpu_ram_share: None,
docker: None,
env: std::collections::BTreeMap::new(),
}],
env: std::collections::BTreeMap::new(),
gpu_ram_share: None,
})
}
pub unsafe fn apply_cuda_visible_devices(devices: &[u8]) {
let joined = devices
.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join(",");
for key in ["CUDA_VISIBLE_DEVICES", "HIP_VISIBLE_DEVICES"] {
if joined.is_empty() {
unsafe { std::env::remove_var(key) };
} else {
unsafe { std::env::set_var(key, &joined) };
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_all_case_insensitive() {
assert_eq!(GpusSpec::parse("all").unwrap(), GpusSpec::All);
assert_eq!(GpusSpec::parse("ALL").unwrap(), GpusSpec::All);
assert_eq!(GpusSpec::parse("All").unwrap(), GpusSpec::All);
}
#[test]
fn parse_single_index() {
assert_eq!(GpusSpec::parse("0").unwrap(), GpusSpec::List(vec![0]));
assert_eq!(GpusSpec::parse("3").unwrap(), GpusSpec::List(vec![3]));
}
#[test]
fn parse_multiple_indices() {
assert_eq!(
GpusSpec::parse("0,1,2").unwrap(),
GpusSpec::List(vec![0, 1, 2])
);
assert_eq!(GpusSpec::parse("3,1").unwrap(), GpusSpec::List(vec![3, 1]));
}
#[test]
fn parse_tolerates_whitespace() {
assert_eq!(
GpusSpec::parse(" 0 , 1 ").unwrap(),
GpusSpec::List(vec![0, 1])
);
assert_eq!(GpusSpec::parse(" all ").unwrap(), GpusSpec::All);
}
#[test]
fn parse_rejects_empty() {
let err = GpusSpec::parse("").unwrap_err();
assert!(err.contains("--gpus requires a value"), "got: {err}");
let err = GpusSpec::parse(" ").unwrap_err();
assert!(err.contains("--gpus requires a value"), "got: {err}");
}
#[test]
fn parse_rejects_empty_entry() {
let err = GpusSpec::parse("0,,1").unwrap_err();
assert!(err.contains("empty entry"), "got: {err}");
let err = GpusSpec::parse(",0").unwrap_err();
assert!(err.contains("empty entry"), "got: {err}");
}
#[test]
fn parse_rejects_non_numeric() {
let err = GpusSpec::parse("0,abc").unwrap_err();
assert!(err.contains("cannot parse"), "got: {err}");
assert!(err.contains("abc"), "got: {err}");
}
#[test]
fn parse_rejects_duplicates() {
let err = GpusSpec::parse("0,1,0").unwrap_err();
assert!(err.contains("duplicate"), "got: {err}");
assert!(err.contains("0"), "got: {err}");
}
#[test]
fn resolve_list_returns_verbatim() {
let r = GpusSpec::List(vec![3, 1]).resolve().unwrap();
assert_eq!(r, vec![3, 1]);
}
#[test]
fn synthesize_local_cluster_basic_shape() {
let c = synthesize_local_cluster(&[0, 1]).unwrap();
assert_eq!(c.controller.host, "127.0.0.1");
assert_eq!(c.workers.len(), 1);
let w = &c.workers[0];
assert_eq!(w.ranks, vec![0, 1]);
assert_eq!(w.local_devices, LocalDevices::Explicit(vec![0, 1]));
assert_eq!(w.nccl_socket_ifname, "lo");
assert!(w.arch.is_none());
assert!(w.ssh.is_none());
assert!(!w.host.trim().is_empty(), "hostname must be non-empty");
assert!(!w.path.trim().is_empty(), "path must be non-empty");
}
#[test]
fn synthesize_local_cluster_validates() {
let c = synthesize_local_cluster(&[0, 1]).unwrap();
c.validate()
.expect("synthesized cluster must pass validate");
}
#[test]
fn synthesize_local_cluster_single_device() {
let c = synthesize_local_cluster(&[2]).unwrap();
c.validate()
.expect("single-device synthesized config validates");
assert_eq!(c.workers[0].ranks, vec![0]);
assert_eq!(c.workers[0].local_devices, LocalDevices::Explicit(vec![2]));
}
#[test]
fn synthesize_local_cluster_rejects_empty() {
let err = synthesize_local_cluster(&[]).unwrap_err();
assert!(err.contains("empty"), "got: {err}");
}
#[test]
fn synthesize_local_cluster_respects_controller_port_env() {
unsafe {
std::env::set_var("FLODL_CONTROLLER_PORT", "31415");
}
let c = synthesize_local_cluster(&[0]).unwrap();
unsafe {
std::env::remove_var("FLODL_CONTROLLER_PORT");
}
assert_eq!(c.controller.port, 31415);
}
}