use std::process::Command;
use crate::cluster::resolve_local_hostname;
use crate::config::{
ClusterConfig, ClusterController, ClusterWorker, LocalDevices,
DEFAULT_CONTROLLER_PORT,
};
#[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 count = count_visible_gpus_via_nvidia_smi()?;
if count == 0 {
return Err(
"--gpus all: nvidia-smi reports 0 GPUs visible. Install \
NVIDIA drivers or specify devices explicitly (e.g. \
--gpus 0)."
.to_string(),
);
}
if count > u8::MAX as usize {
return Err(format!(
"--gpus all: nvidia-smi reports {count} GPUs which \
exceeds the supported device-index range (0..255). \
Specify devices explicitly via --gpus."
));
}
Ok((0u8..count as u8).collect())
}
}
}
}
pub fn count_visible_gpus_via_nvidia_smi() -> Result<usize, String> {
let out = Command::new("nvidia-smi")
.arg("-L")
.output()
.map_err(|e| {
format!(
"failed to run `nvidia-smi -L`: {e}. Install NVIDIA drivers \
or specify devices explicitly (e.g. --gpus 0)."
)
})?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
return Err(format!(
"`nvidia-smi -L` exited non-zero: {}",
stderr.trim()
));
}
let stdout = String::from_utf8_lossy(&out.stdout);
Ok(stdout.lines().filter(|l| l.starts_with("GPU ")).count())
}
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,
docker: None,
env: std::collections::BTreeMap::new(),
}],
env: std::collections::BTreeMap::new(),
})
}
pub unsafe fn apply_cuda_visible_devices(devices: &[u8]) {
let joined = devices
.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join(",");
if joined.is_empty() {
unsafe { std::env::remove_var("CUDA_VISIBLE_DEVICES") };
} else {
unsafe { std::env::set_var("CUDA_VISIBLE_DEVICES", &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);
}
}