use crate::distributed::launcher::{FullCluster, FullWorker};
use crate::distributed::wire::SESSION_SALT_BYTES;
pub const ENV_TESTING_CLUSTER_JSON: &str = "FLODL_TESTING_CLUSTER_JSON";
pub fn discover_test_cluster() -> Option<FullCluster> {
if let Ok(raw) = std::env::var(ENV_TESTING_CLUSTER_JSON) {
return Some(parse_env_cluster(&raw));
}
if let Some(n) = autodetect_local_gpus() {
return Some(synthesize_local_cluster(n));
}
None
}
fn parse_env_cluster(raw: &str) -> FullCluster {
let bytes = crate::distributed::cluster::hex_decode(raw.trim())
.unwrap_or_else(|e| {
panic!(
"{ENV_TESTING_CLUSTER_JSON} hex-decode failed: {e}. \
The value must be a hex-encoded canonical-JSON cluster \
envelope (as written by fdl-cli when --env activates \
an overlay with a cluster: block)."
)
});
let val: serde_json::Value = serde_json::from_slice(&bytes)
.unwrap_or_else(|e| {
panic!("{ENV_TESTING_CLUSTER_JSON} JSON parse failed: {e}")
});
FullCluster::from_value(&val)
.unwrap_or_else(|e| {
panic!("{ENV_TESTING_CLUSTER_JSON} schema violation: {e}")
})
}
fn autodetect_local_gpus() -> Option<usize> {
let count = crate::tensor::cuda_device_count();
if count > 0 {
Some(count as usize)
} else {
None
}
}
fn synthesize_local_cluster(n_gpus: usize) -> FullCluster {
let ranks: Vec<usize> = (0..n_gpus).collect();
let local_devices: Vec<u8> = (0..n_gpus as u8).collect();
FullCluster {
controller: super::launcher::FullController {
host: "127.0.0.1".to_string(),
port: 0,
path: String::new(),
docker: None,
arch: None,
data_path: None,
join: None,
},
workers: vec![FullWorker {
host: "localhost".to_string(),
ranks,
local_devices: Some(local_devices),
nccl_socket_ifname: "lo".to_string(),
path: String::new(),
arch: None,
ssh: None,
tunnel: false,
env: std::collections::BTreeMap::new(),
}],
salt: [0u8; SESSION_SALT_BYTES],
env: std::collections::BTreeMap::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn unset() {
unsafe { std::env::remove_var(ENV_TESTING_CLUSTER_JSON) };
}
#[test]
fn discover_returns_none_without_env_or_cuda() {
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
unset();
let result = discover_test_cluster();
let _ = result;
}
#[test]
fn discover_reads_env_var_when_set() {
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let canonical = serde_json::json!({
"controller": { "host": "127.0.0.1", "port": 8888, "path": "/tmp" },
"workers": [{
"host": "test-host",
"ranks": [0, 1],
"local_devices": [0, 1],
"nccl_socket_ifname": "lo",
"path": "/tmp",
"arch": null,
"ssh": null,
}],
"session_salt": "0".repeat(SESSION_SALT_BYTES * 2),
});
let bytes = serde_json::to_vec(&canonical).unwrap();
let hex = bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut s, b| {
s.push_str(&format!("{b:02x}"));
s
});
unsafe { std::env::set_var(ENV_TESTING_CLUSTER_JSON, &hex) };
let cluster = discover_test_cluster().expect("env-driven topology");
assert_eq!(cluster.controller.host, "127.0.0.1");
assert_eq!(cluster.controller.port, 8888);
assert_eq!(cluster.workers.len(), 1);
assert_eq!(cluster.workers[0].host, "test-host");
assert_eq!(cluster.workers[0].ranks, vec![0, 1]);
unset();
}
#[test]
fn synthesize_local_cluster_shape() {
let c = synthesize_local_cluster(3);
assert_eq!(c.controller.host, "127.0.0.1");
assert_eq!(c.controller.port, 0);
assert_eq!(c.workers.len(), 1);
assert_eq!(c.workers[0].host, "localhost");
assert_eq!(c.workers[0].ranks, vec![0, 1, 2]);
assert_eq!(
c.workers[0].local_devices.as_deref().unwrap(),
&[0u8, 1, 2][..]
);
assert!(c.workers[0].ssh.is_none());
assert_eq!(c.salt, [0u8; SESSION_SALT_BYTES]);
}
#[test]
#[should_panic(expected = "hex-decode failed")]
fn discover_panics_on_malformed_env_hex() {
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
unsafe { std::env::set_var(ENV_TESTING_CLUSTER_JSON, "not-valid-hex-zz") };
let _ = discover_test_cluster();
unset();
}
}