use std::net::TcpStream;
use std::process::Stdio;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::distributed::wire::{
CHANNEL_MAGIC_JOIN, ControlFrame, JoinMsgWire, MsgKind, SESSION_SALT_BYTES,
SessionSalt, connect_with_retry, salt_from_hex, scaled_deadline_secs,
write_channel_magic, write_stall_timeout,
};
use crate::tensor::{Result, TensorError};
use super::spawn::{build_local_relay_command, build_local_spawn_command, forward_lines};
use super::ENV_AGENT_JSON;
const JOIN_REPLY_TIMEOUT_SECS: u64 = 30;
const FORMATION_WAIT_MARGIN_SECS: u64 = 30;
const SUPERVISE_POLL: Duration = Duration::from_millis(50);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentSpec {
pub host: String,
pub controller_host: String,
pub controller_port: u16,
#[serde(default)]
pub salt_hex: Option<String>,
#[serde(default)]
pub local_devices: Option<Vec<u8>>,
#[serde(default)]
pub libtorch: String,
#[serde(default)]
pub dataset_sig_hex: Option<String>,
}
impl AgentSpec {
pub fn from_env() -> Result<Self> {
let raw = std::env::var(ENV_AGENT_JSON).map_err(|e| {
TensorError::new(&format!("cluster agent: {ENV_AGENT_JSON} unreadable: {e}"))
})?;
let bytes = crate::distributed::cluster::hex_decode(raw.trim())
.map_err(|e| TensorError::new(&format!("cluster agent: spec hex-decode: {e}")))?;
serde_json::from_slice(&bytes)
.map_err(|e| TensorError::new(&format!("cluster agent: spec JSON parse: {e}")))
}
pub fn to_env_hex(&self) -> Result<String> {
let json = serde_json::to_string(self).map_err(|e| {
TensorError::new(&format!("cluster agent: spec JSON encode: {e}"))
})?;
Ok(crate::distributed::cluster::hex_encode(json.as_bytes()))
}
}
#[derive(Debug)]
pub(crate) struct JoinOutcome {
pub salt: SessionSalt,
pub ranks: Vec<u32>,
pub envelope_hex: String,
pub relay_spec_hex: Option<String>,
pub stream: TcpStream,
}
pub(crate) fn join_world(
controller_host: &str,
controller_port: u16,
pre_shared: Option<SessionSalt>,
hello: JoinMsgWire,
) -> Result<JoinOutcome> {
use std::net::ToSocketAddrs;
let addr = (controller_host, controller_port)
.to_socket_addrs()
.map_err(|e| {
TensorError::new(&format!(
"cluster agent: resolve {controller_host}:{controller_port}: {e}"
))
})?
.next()
.ok_or_else(|| {
TensorError::new(&format!(
"cluster agent: no address for {controller_host}:{controller_port}"
))
})?;
let mut stream = connect_with_retry(addr, "cluster agent join")?;
let _ = stream.set_nodelay(true);
stream
.set_write_timeout(Some(write_stall_timeout()))
.map_err(|e| TensorError::new(&format!("cluster agent: set_write_timeout: {e}")))?;
stream
.set_read_timeout(Some(Duration::from_secs(scaled_deadline_secs(
JOIN_REPLY_TIMEOUT_SECS,
))))
.map_err(|e| TensorError::new(&format!("cluster agent: set_read_timeout: {e}")))?;
crate::distributed::wire::warn_cleartext_public_peer(
"cluster agent join",
addr,
);
write_channel_magic(&mut stream, CHANNEL_MAGIC_JOIN)?;
let join_key = pre_shared.unwrap_or([0u8; SESSION_SALT_BYTES]);
ControlFrame::encode(&join_key, MsgKind::Join, &hello)?.write_to(&mut stream)?;
let reply = ControlFrame::read_from(&mut stream, &join_key)?.ok_or_else(|| {
TensorError::new(
"cluster agent: controller closed the connection before replying — \
under pre-shared admission this usually means a session-salt \
mismatch (frame authentication failed on the controller); \
otherwise the controller went away mid-join",
)
})?;
let (ranks, salt, formation_wait_secs) = match reply.decode::<JoinMsgWire>()? {
JoinMsgWire::Accept { ranks, salt_hex, formation_wait_secs } => {
let salt = match (pre_shared, salt_hex) {
(Some(s), _) => s,
(None, Some(hex)) => salt_from_hex(&hex)?,
(None, None) => {
return Err(TensorError::new(
"cluster agent: open-admission accept carried no session \
salt — controller and worker disagree on the trust mode",
));
}
};
(ranks, salt, formation_wait_secs)
}
JoinMsgWire::Reject { reason } => {
return Err(TensorError::new(&format!(
"cluster agent: join REJECTED by the controller: {reason}"
)));
}
other => {
return Err(TensorError::new(&format!(
"cluster agent: expected Accept or Reject, got {other:?}"
)));
}
};
eprintln!(
"cluster agent: joined as rank(s) {ranks:?}; waiting for world \
formation (up to {formation_wait_secs}s)"
);
stream
.set_read_timeout(Some(Duration::from_secs(
formation_wait_secs.saturating_add(FORMATION_WAIT_MARGIN_SECS),
)))
.map_err(|e| TensorError::new(&format!("cluster agent: set_read_timeout: {e}")))?;
let frame = ControlFrame::read_from(&mut stream, &salt)?.ok_or_else(|| {
TensorError::new(
"cluster agent: controller closed the connection while waiting for \
world formation",
)
})?;
match frame.decode::<JoinMsgWire>()? {
JoinMsgWire::WorldFormed { envelope_hex, relay_spec_hex } => Ok(JoinOutcome {
salt,
ranks,
envelope_hex,
relay_spec_hex,
stream,
}),
JoinMsgWire::Abort { reason } => Err(TensorError::new(&format!(
"cluster agent: run aborted before world formation: {reason}"
))),
other => Err(TensorError::new(&format!(
"cluster agent: expected WorldFormed or Abort, got {other:?}"
))),
}
}
pub(crate) fn resolve_devices(requested: Option<&Vec<u8>>) -> (Vec<u8>, Vec<String>) {
let detected = crate::sys::detect_gpus();
let devices: Vec<u8> = match requested {
Some(d) => d.clone(),
None => detected.iter().map(|g| g.index).collect(),
};
let labels = devices
.iter()
.map(|d| {
detected
.iter()
.find(|g| g.index == *d)
.map(|g| {
format!(
"{} ({}GB, {})",
g.name,
g.total_memory_mb / 1024,
g.sm_version(),
)
})
.unwrap_or_else(|| format!("cuda:{d}"))
})
.collect();
(devices, labels)
}
fn build_hello(spec: &AgentSpec, devices: &[u8], gpus: Vec<String>) -> Result<JoinMsgWire> {
let dataset_sig: [u8; 32] = match &spec.dataset_sig_hex {
None => [0u8; 32],
Some(hex) => {
let bytes = crate::distributed::cluster::hex_decode(hex.trim()).map_err(|e| {
TensorError::new(&format!("cluster agent: dataset_sig hex-decode: {e}"))
})?;
bytes.try_into().map_err(|_| {
TensorError::new("cluster agent: dataset_sig must be 32 bytes (64 hex chars)")
})?
}
};
Ok(JoinMsgWire::Hello {
host: spec.host.clone(),
local_devices: devices.to_vec(),
gpus,
libtorch: spec.libtorch.clone(),
dataset_sig,
})
}
pub fn run_agent() -> Result<()> {
let spec = AgentSpec::from_env()?;
let (devices, gpus) = resolve_devices(spec.local_devices.as_ref());
if devices.is_empty() {
return Err(TensorError::new(&format!(
"cluster agent: host {:?} has no GPUs to offer (none detected and \
no local_devices configured)",
spec.host,
)));
}
let pre_shared = spec
.salt_hex
.as_deref()
.map(salt_from_hex)
.transpose()?;
eprintln!(
"cluster agent: host {:?} dialing controller {}:{} with {} rank(s) \
(devices {:?}, admission: {})",
spec.host,
spec.controller_host,
spec.controller_port,
devices.len(),
devices,
if pre_shared.is_some() { "pre-shared salt" } else { "open" },
);
let hello = build_hello(&spec, &devices, gpus)?;
let outcome = join_world(
&spec.controller_host,
spec.controller_port,
pre_shared,
hello,
)?;
let children = spawn_host_children(
&spec.host,
&devices,
&outcome,
&std::collections::BTreeMap::new(),
)?;
supervise(&spec.host, children, outcome.stream, &outcome.salt)
}
pub(crate) fn join_and_spawn_local(
spec: AgentSpec,
extra_env: &std::collections::BTreeMap<String, String>,
) -> Result<Vec<HostChild>> {
let (devices, gpus) = resolve_devices(spec.local_devices.as_ref());
if devices.is_empty() {
return Err(TensorError::new(&format!(
"cluster launcher: local worker {:?} has no GPUs to offer (none \
detected and no local_devices configured)",
spec.host,
)));
}
let pre_shared = spec
.salt_hex
.as_deref()
.map(salt_from_hex)
.transpose()?;
let hello = build_hello(&spec, &devices, gpus)?;
let outcome = join_world(
&spec.controller_host,
spec.controller_port,
pre_shared,
hello,
)?;
spawn_host_children(&spec.host, &devices, &outcome, extra_env)
}
pub(crate) struct HostChild {
pub label: String,
pub slot: usize,
pub rank: Option<u32>,
pub child: std::process::Child,
pub forwarders: Vec<thread::JoinHandle<()>>,
}
pub(crate) fn spawn_host_children(
host: &str,
devices: &[u8],
outcome: &JoinOutcome,
extra_env: &std::collections::BTreeMap<String, String>,
) -> Result<Vec<HostChild>> {
if outcome.ranks.len() != devices.len() {
return Err(TensorError::new(&format!(
"cluster agent: controller assigned {} rank(s) for {} device(s) — \
protocol violation",
outcome.ranks.len(),
devices.len(),
)));
}
let exe = std::env::current_exe().map_err(|e| {
TensorError::new(&format!("cluster agent: current_exe() failed: {e}"))
})?;
let user_args: Vec<String> = std::env::args().skip(1).collect();
let mut children: Vec<HostChild> = Vec::with_capacity(devices.len() + 1);
let spawn_result: Result<()> = (|| {
if let Some(relay_spec_hex) = &outcome.relay_spec_hex {
let mut cmd = build_local_relay_command(&exe, &user_args, relay_spec_hex);
for (k, v) in extra_env {
cmd.env(k, v);
}
cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
let mut child = cmd.spawn().map_err(|e| {
TensorError::new(&format!("cluster agent: spawn relay failed: {e}"))
})?;
let prefix = format!("[{host}:relay] ");
let mut forwarders = Vec::with_capacity(2);
if let Some(out) = child.stdout.take() {
let p = prefix.clone();
forwarders.push(thread::spawn(move || forward_lines(out, p, false)));
}
if let Some(err) = child.stderr.take() {
let p = prefix;
forwarders.push(thread::spawn(move || forward_lines(err, p, true)));
}
children.push(HostChild {
label: format!("relay of {host}"),
slot: super::spawn::RELAY_RANK_SENTINEL,
rank: None,
child,
forwarders,
});
}
for (local_rank, (&phys, &grank)) in
devices.iter().zip(outcome.ranks.iter()).enumerate()
{
let mut cmd = build_local_spawn_command(
&exe,
&user_args,
&outcome.envelope_hex,
local_rank,
Some(phys),
);
for (k, v) in extra_env {
cmd.env(k, v);
}
cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
let mut child = cmd.spawn().map_err(|e| {
TensorError::new(&format!(
"cluster agent: spawn rank {grank} (device {phys}) failed: {e}"
))
})?;
let prefix = format!("[{host}:{phys}:r{grank}] ");
let mut forwarders = Vec::with_capacity(2);
if let Some(out) = child.stdout.take() {
let p = prefix.clone();
forwarders.push(thread::spawn(move || forward_lines(out, p, false)));
}
if let Some(err) = child.stderr.take() {
let p = prefix;
forwarders.push(thread::spawn(move || forward_lines(err, p, true)));
}
children.push(HostChild {
label: format!("rank {grank} of {host}"),
slot: local_rank,
rank: Some(grank),
child,
forwarders,
});
}
Ok(())
})();
if let Err(e) = spawn_result {
eprintln!(
"cluster agent: spawn failed; terminating {} already-spawned \
child(ren): {e}",
children.len(),
);
for mut c in children {
let _ = c.child.kill();
let _ = c.child.wait();
for f in c.forwarders {
let _ = f.join();
}
}
return Err(e);
}
Ok(children)
}
fn supervise(
host: &str,
mut children: Vec<HostChild>,
stream: TcpStream,
salt: &SessionSalt,
) -> Result<()> {
let abort = Arc::new(AtomicBool::new(false));
let abort_reason = Arc::new(std::sync::Mutex::new(String::new()));
let mut reader = stream.try_clone().map_err(|e| {
TensorError::new(&format!("cluster agent: join stream try_clone: {e}"))
})?;
reader
.set_read_timeout(None)
.map_err(|e| TensorError::new(&format!("cluster agent: reader timeout reset: {e}")))?;
let abort_r = Arc::clone(&abort);
let reason_r = Arc::clone(&abort_reason);
let salt_r = *salt;
let reader_handle = thread::spawn(move || {
loop {
match ControlFrame::read_from(&mut reader, &salt_r) {
Ok(Some(frame)) => match frame.decode::<JoinMsgWire>() {
Ok(JoinMsgWire::Abort { reason }) => {
if let Ok(mut r) = reason_r.lock() {
*r = reason;
}
abort_r.store(true, Ordering::SeqCst);
return;
}
Ok(other) => {
crate::verbose!(
" cluster agent: unexpected control-link message \
{other:?}; ignoring"
);
}
Err(e) => {
eprintln!("cluster agent: control-link decode error: {e}");
}
},
Ok(None) => {
if let Ok(mut r) = reason_r.lock() {
*r = "controller closed the join connection".to_string();
}
abort_r.store(true, Ordering::SeqCst);
return;
}
Err(e) => {
if let Ok(mut r) = reason_r.lock() {
*r = format!("join connection error: {e}");
}
abort_r.store(true, Ordering::SeqCst);
return;
}
}
}
});
let mut writer = stream;
let mut rank_failures: usize = 0;
let mut aborted = false;
let mut relay_died_early = false;
let mut live = children.len();
let mut reaped: Vec<bool> = vec![false; children.len()];
while live > 0 {
if !aborted && abort.load(Ordering::SeqCst) {
aborted = true;
let reason = abort_reason
.lock()
.map(|r| r.clone())
.unwrap_or_default();
eprintln!(
"cluster agent: tearing down host {host:?} ({reason}); killing \
{live} child(ren)"
);
for (i, c) in children.iter_mut().enumerate() {
if !reaped[i] {
let _ = c.child.kill();
}
}
}
let mut progressed = false;
for i in 0..children.len() {
if reaped[i] {
continue;
}
match children[i].child.try_wait() {
Ok(Some(status)) => {
reaped[i] = true;
live -= 1;
progressed = true;
let code = status.code().unwrap_or(-1);
let clean = status.success();
if !clean {
eprintln!(
"cluster agent: {} exited with {status}",
children[i].label,
);
}
match children[i].rank {
Some(rank) => {
if !clean {
rank_failures += 1;
}
let msg = JoinMsgWire::RankExited { rank, code };
let _ = ControlFrame::encode(salt, MsgKind::Join, &msg)
.and_then(|f| f.write_to(&mut writer));
}
None => {
if !clean && live > 0 && !aborted {
relay_died_early = true;
eprintln!(
"cluster agent: relay died with {live} rank \
child(ren) still running — tearing down \
host {host:?}"
);
for (j, c) in children.iter_mut().enumerate() {
if !reaped[j] {
let _ = c.child.kill();
}
}
}
}
}
}
Ok(None) => {}
Err(e) => {
reaped[i] = true;
live -= 1;
progressed = true;
eprintln!(
"cluster agent: wait on {} failed: {e}",
children[i].label,
);
}
}
}
if !progressed {
thread::sleep(SUPERVISE_POLL);
}
}
for c in children {
for f in c.forwarders {
let _ = f.join();
}
}
let _ = writer.shutdown(std::net::Shutdown::Both);
let _ = reader_handle.join();
if aborted {
let reason = abort_reason.lock().map(|r| r.clone()).unwrap_or_default();
return Err(TensorError::new(&format!(
"cluster agent: host {host:?} torn down: {reason}"
)));
}
if relay_died_early {
return Err(TensorError::new(&format!(
"cluster agent: host {host:?} torn down: relay died mid-run"
)));
}
if rank_failures > 0 {
eprintln!(
"cluster agent: host {host:?} finished DEGRADED — {rank_failures} \
rank child(ren) exited non-zero (reported to the controller)"
);
} else {
eprintln!("cluster agent: host {host:?} finished cleanly");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::distributed::wire::{expect_channel_magic, salt_to_hex};
use std::net::TcpListener;
fn test_salt() -> SessionSalt {
[7u8; SESSION_SALT_BYTES]
}
fn test_hello() -> JoinMsgWire {
JoinMsgWire::Hello {
host: "worker-x".to_string(),
local_devices: vec![0, 1],
gpus: vec!["T".to_string(), "T".to_string()],
libtorch: "builds/test".to_string(),
dataset_sig: [0u8; 32],
}
}
fn fake_controller(
key: SessionSalt,
salt: SessionSalt,
replies: Vec<JoinMsgWire>,
) -> (u16, std::thread::JoinHandle<JoinMsgWire>) {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
expect_channel_magic(&mut stream, CHANNEL_MAGIC_JOIN, "fake controller")
.unwrap();
let frame = ControlFrame::read_from(&mut stream, &key)
.unwrap()
.expect("hello frame");
let hello: JoinMsgWire = frame.decode().unwrap();
for (i, msg) in replies.iter().enumerate() {
let frame_key = if i == 0 { key } else { salt };
ControlFrame::encode(&frame_key, MsgKind::Join, msg)
.unwrap()
.write_to(&mut stream)
.unwrap();
}
hello
});
(port, handle)
}
#[test]
fn agent_spec_round_trips_through_hex() {
let spec = AgentSpec {
host: "pascal".to_string(),
controller_host: "192.168.122.1".to_string(),
controller_port: 1337,
salt_hex: Some(salt_to_hex(&test_salt())),
local_devices: Some(vec![0, 1]),
libtorch: "builds/sm61-sm120".to_string(),
dataset_sig_hex: None,
};
let hex = spec.to_env_hex().unwrap();
let bytes = crate::distributed::cluster::hex_decode(&hex).unwrap();
let parsed: AgentSpec = serde_json::from_slice(&bytes).unwrap();
assert_eq!(parsed, spec);
let minimal: AgentSpec = serde_json::from_str(
r#"{"host":"cloud-1","controller_host":"10.0.0.1","controller_port":1337}"#,
)
.unwrap();
assert_eq!(minimal.salt_hex, None);
assert_eq!(minimal.local_devices, None);
assert_eq!(minimal.dataset_sig_hex, None);
}
#[test]
fn join_world_open_admission_receives_salt_and_artifacts() {
let salt = test_salt();
let zero: SessionSalt = [0u8; SESSION_SALT_BYTES];
let (port, controller) = fake_controller(
zero,
salt,
vec![
JoinMsgWire::Accept {
ranks: vec![3, 4],
salt_hex: Some(salt_to_hex(&salt)),
formation_wait_secs: 60,
},
JoinMsgWire::WorldFormed {
envelope_hex: "aa11".to_string(),
relay_spec_hex: Some("bb22".to_string()),
},
],
);
let outcome = join_world("127.0.0.1", port, None, test_hello()).unwrap();
assert_eq!(outcome.salt, salt);
assert_eq!(outcome.ranks, vec![3, 4]);
assert_eq!(outcome.envelope_hex, "aa11");
assert_eq!(outcome.relay_spec_hex.as_deref(), Some("bb22"));
assert_eq!(controller.join().unwrap(), test_hello());
}
#[test]
fn join_world_pre_shared_keys_hello_with_the_salt() {
let salt = test_salt();
let (port, controller) = fake_controller(
salt,
salt,
vec![
JoinMsgWire::Accept {
ranks: vec![0],
salt_hex: None,
formation_wait_secs: 60,
},
JoinMsgWire::WorldFormed {
envelope_hex: String::new(),
relay_spec_hex: None,
},
],
);
let outcome = join_world("127.0.0.1", port, Some(salt), test_hello()).unwrap();
assert_eq!(outcome.salt, salt);
controller.join().unwrap();
}
#[test]
fn join_world_reject_and_abort_are_loud() {
let salt = test_salt();
let zero: SessionSalt = [0u8; SESSION_SALT_BYTES];
let (port, controller) = fake_controller(
zero,
salt,
vec![JoinMsgWire::Reject { reason: "dataset signature mismatch".to_string() }],
);
let err = join_world("127.0.0.1", port, None, test_hello())
.unwrap_err()
.to_string();
assert!(err.contains("REJECTED"), "got: {err}");
assert!(err.contains("dataset signature mismatch"), "got: {err}");
controller.join().unwrap();
let (port, controller) = fake_controller(
zero,
salt,
vec![
JoinMsgWire::Accept {
ranks: vec![0],
salt_hex: Some(salt_to_hex(&salt)),
formation_wait_secs: 60,
},
JoinMsgWire::Abort { reason: "quorum not met".to_string() },
],
);
let err = join_world("127.0.0.1", port, None, test_hello())
.unwrap_err()
.to_string();
assert!(err.contains("aborted"), "got: {err}");
assert!(err.contains("quorum not met"), "got: {err}");
controller.join().unwrap();
}
#[test]
fn join_world_open_accept_without_salt_is_a_trust_mode_error() {
let salt = test_salt();
let zero: SessionSalt = [0u8; SESSION_SALT_BYTES];
let (port, controller) = fake_controller(
zero,
salt,
vec![JoinMsgWire::Accept {
ranks: vec![0],
salt_hex: None,
formation_wait_secs: 60,
}],
);
let err = join_world("127.0.0.1", port, None, test_hello())
.unwrap_err()
.to_string();
assert!(err.contains("trust mode"), "got: {err}");
controller.join().unwrap();
}
}