use std::env;
use std::process::Stdio;
use std::sync::{Arc, OnceLock};
use std::thread;
use serde::{Deserialize, Serialize};
pub use crate::distributed::membership::JoinedMember;
use crate::distributed::relay::agent::{ChannelKind, RelayChannel};
use crate::distributed::relay::{RELAY_CONTROL_LOOPBACK_OFFSET, RELAY_DATA_LOOPBACK_OFFSET};
use crate::tensor::{Result, TensorError};
mod agent;
mod spawn;
mod types;
#[cfg(test)]
mod tests;
pub use agent::{AgentSpec, run_agent};
pub use types::{SshConfig, FullCluster, FullController, FullWorker, JoinKnobs};
use spawn::{
load_prebuild_envelope, supervise_children, ElasticSupervision,
build_ssh_spawn_command, cleanup_remote_hosts_parallel,
build_remote_agent_bash_command, build_slim_envelope_for, forward_lines,
AGENT_RANK_SENTINEL,
};
static COHORT_INVENTORY: OnceLock<Vec<JoinedMember>> = OnceLock::new();
pub fn cohort_inventory() -> Option<&'static [JoinedMember]> {
COHORT_INVENTORY.get().map(|v| v.as_slice())
}
pub const ENV_FULL_CLUSTER_JSON: &str = "FLODL_INTERNAL_FULL_CLUSTER_JSON";
pub const ENV_RELAY_JSON: &str = "FLODL_INTERNAL_RELAY_JSON";
pub const ENV_AGENT_JSON: &str = "FLODL_INTERNAL_AGENT_JSON";
pub const ENV_FDL_CMD: &str = "FLODL_INTERNAL_FDL_CMD";
pub const ENV_FDL_ENV: &str = "FDL_ENV";
pub const ENV_PREBUILD_PER_HOST: &str = "FLODL_INTERNAL_PREBUILD_PER_HOST";
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Role {
SingleDevice,
Rank,
Launcher,
Relay,
Agent,
}
pub fn dispatch() -> Result<Role> {
let agent_set = env::var_os(ENV_AGENT_JSON).is_some();
let relay_set = env::var_os(ENV_RELAY_JSON).is_some();
let full_set = env::var_os(ENV_FULL_CLUSTER_JSON).is_some();
let slim_set = env::var_os(crate::distributed::cluster::ENV_CLUSTER_JSON).is_some();
let slot_set = env::var_os(crate::distributed::cluster::ENV_LOCAL_RANK).is_some();
match (agent_set, relay_set, full_set, slim_set, slot_set) {
(false, false, false, false, false) => Ok(Role::SingleDevice),
(false, false, false, true, true) => Ok(Role::Rank),
(false, false, true, false, false) => Ok(Role::Launcher),
(false, true, false, false, false) => Ok(Role::Relay),
(true, false, false, false, false) => Ok(Role::Agent),
_ => Err(TensorError::new(&format!(
"cluster launcher: inconsistent env (FLODL_INTERNAL_AGENT_JSON={}, \
FLODL_INTERNAL_RELAY_JSON={}, \
FLODL_INTERNAL_FULL_CLUSTER_JSON={}, FLODL_INTERNAL_CLUSTER_JSON={}, FLODL_INTERNAL_LOCAL_RANK={}). \
Expected: all-unset (single-device), slim+slot only (rank), \
full only (launcher), relay only (relay), or agent only (agent).",
on_off(agent_set),
on_off(relay_set),
on_off(full_set),
on_off(slim_set),
on_off(slot_set),
))),
}
}
pub(crate) fn role_env_pristine() -> bool {
matches!(dispatch(), Ok(Role::SingleDevice))
}
pub fn exit_if_worker_role() {
use crate::distributed::ddp_run::clean_process_exit;
if env::var_os(ENV_RELAY_JSON).is_some() {
match run_relay() {
Ok(()) => clean_process_exit(0),
Err(e) => {
eprintln!("flodl relay: {e}");
clean_process_exit(1);
}
}
}
if env::var_os(ENV_AGENT_JSON).is_some() {
match run_agent() {
Ok(()) => clean_process_exit(0),
Err(e) => {
eprintln!("flodl agent: {e}");
clean_process_exit(1);
}
}
}
}
pub(crate) fn promote_programmatic_cluster(full: &FullCluster) -> bool {
if !role_env_pristine() {
crate::debug!(
"cluster: role env already set; skipping programmatic cluster promotion"
);
return false;
}
let hex =
crate::distributed::cluster::hex_encode(full.to_json().to_string().as_bytes());
unsafe { std::env::set_var(ENV_FULL_CLUSTER_JSON, hex) };
true
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RelaySpec {
pub host: String,
pub controller_host: String,
pub controller_port: u16,
pub ranks: Vec<u32>,
pub salt_hex: String,
pub world_size: usize,
pub data_channel: bool,
#[serde(default)]
pub frame_ceiling_bytes: usize,
}
pub fn run_relay() -> Result<()> {
let raw = env::var(ENV_RELAY_JSON)
.map_err(|e| TensorError::new(&format!("relay: {ENV_RELAY_JSON} unreadable: {e}")))?;
let bytes = crate::distributed::cluster::hex_decode(&raw)
.map_err(|e| TensorError::new(&format!("relay: spec hex-decode: {e}")))?;
let spec: RelaySpec = serde_json::from_slice(&bytes)
.map_err(|e| TensorError::new(&format!("relay: spec JSON parse: {e}")))?;
crate::distributed::wire::set_frame_ceiling(spec.frame_ceiling_bytes);
let salt = crate::distributed::wire::salt_from_hex(&spec.salt_hex)?;
let base = spec.controller_port;
let loopback = |off: u16| -> Result<std::net::SocketAddr> {
format!("127.0.0.1:{}", base.saturating_add(off))
.parse()
.map_err(|e| TensorError::new(&format!("relay: loopback addr: {e}")))
};
let resolve = |host: &str, port: u16| -> Result<std::net::SocketAddr> {
use std::net::ToSocketAddrs;
(host, port)
.to_socket_addrs()
.map_err(|e| TensorError::new(&format!("relay: resolve {host}:{port}: {e}")))?
.next()
.ok_or_else(|| TensorError::new(&format!("relay: no address for {host}:{port}")))
};
eprintln!(
"cluster relay: host '{}' ranks {:?} -> controller {}:{} (data_channel={})",
spec.host, spec.ranks, spec.controller_host, base, spec.data_channel,
);
let (ctrl_listener, _) = RelayChannel::bind(loopback(RELAY_CONTROL_LOOPBACK_OFFSET)?)?;
let ctrl_upstream = resolve(&spec.controller_host, base)?;
let data_handle = if spec.data_channel {
let (data_listener, _) = RelayChannel::bind(loopback(RELAY_DATA_LOOPBACK_OFFSET)?)?;
let data_upstream = resolve(&spec.controller_host, base)?;
let host = spec.host.clone();
let ranks = spec.ranks.clone();
let ws = spec.world_size;
Some(
thread::Builder::new()
.name("flodl-relay-data".into())
.spawn(move || -> Result<()> {
RelayChannel::start(
data_listener,
ChannelKind::Data,
data_upstream,
host,
ranks,
ws,
salt,
)?
.join()
})
.map_err(|e| TensorError::new(&format!("relay: spawn data thread: {e}")))?,
)
} else {
None
};
RelayChannel::start(
ctrl_listener,
ChannelKind::Control,
ctrl_upstream,
spec.host.clone(),
spec.ranks.clone(),
spec.world_size,
salt,
)?
.join()?;
if let Some(h) = data_handle {
h.join()
.map_err(|_| TensorError::new("relay: data thread panicked"))??;
}
eprintln!("cluster relay: host '{}' shut down cleanly", spec.host);
Ok(())
}
fn on_off(b: bool) -> &'static str {
if b { "set" } else { "unset" }
}
static CLUSTER_ENTRY_CONSUMED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
pub(crate) fn claim_cluster_entry(role: &str) -> Result<()> {
if CLUSTER_ENTRY_CONSUMED.swap(true, std::sync::atomic::Ordering::SeqCst) {
return Err(TensorError::new(&format!(
"cluster {role}: a cluster training session was already run in \
this process — the launcher infrastructure (rendezvous, relays, \
coordinator) is per-session and has shut down. Run one cluster \
`Trainer::run` per process: loop at the process level (e.g. \
invoke the binary once per model) instead of looping inside it.",
)));
}
Ok(())
}
fn validate_tunnel_topology(
full: &FullCluster,
local_host_name: &str,
backend_is_nccl: bool,
) -> Result<bool> {
let tunneled: Vec<&FullWorker> =
full.workers.iter().filter(|w| w.tunnel).collect();
if tunneled.is_empty() {
return Ok(false);
}
if let Some(local) = tunneled.iter().find(|w| w.host == local_host_name) {
return Err(TensorError::new(&format!(
"cluster launcher: worker {:?} sets `tunnel: true` but runs on \
the launcher host — there is no SSH session to carry a \
forward, and loopback already reaches the controller. Remove \
the flag from this host.",
local.host,
)));
}
if backend_is_nccl {
return Err(TensorError::new(&format!(
"cluster launcher: worker(s) {:?} set `tunnel: true` but the \
run uses an NCCL backend. NCCL's data plane is peer-to-peer \
and cannot ride a controller tunnel; tunnel mode requires a \
CPU ElChe mode (cpu_sync / cpu_cadence / cpu_async), whose \
traffic all flows through the per-host relay's single \
upstream connection.",
tunneled.iter().map(|w| w.host.as_str()).collect::<Vec<_>>(),
)));
}
Ok(full
.workers
.iter()
.filter(|w| w.host != local_host_name)
.all(|w| w.tunnel))
}
fn derive_join_config(
knobs: Option<&JoinKnobs>,
capacity: usize,
) -> crate::distributed::membership::JoinConfig {
let defaults = crate::distributed::membership::JoinConfig::default();
let knobs = knobs.cloned().unwrap_or_default();
let join_timeout_secs = knobs.join_timeout_secs.unwrap_or(defaults.join_timeout_secs);
crate::distributed::membership::JoinConfig {
min_rank_start: knobs.min_rank_start.unwrap_or(capacity),
join_timeout_secs,
target_ranks: Some(knobs.target_ranks.unwrap_or(capacity)),
max_join_timeout_secs: knobs
.max_join_timeout_secs
.unwrap_or(defaults.max_join_timeout_secs.max(join_timeout_secs)),
open_admission: knobs.open_admission.unwrap_or(false),
}
}
fn synthesize_world<'a>(
config: &FullCluster,
members: impl Iterator<Item = &'a crate::distributed::membership::JoinedMember>,
salt: crate::distributed::wire::SessionSalt,
) -> FullCluster {
let workers: Vec<FullWorker> = members
.map(|m| match config.workers.iter().find(|w| w.host == m.host) {
Some(w) => FullWorker {
ranks: m.ranks.clone(),
local_devices: Some(m.local_devices.clone()),
..w.clone()
},
None => FullWorker {
host: m.host.clone(),
ranks: m.ranks.clone(),
local_devices: Some(m.local_devices.clone()),
nccl_socket_ifname: String::new(),
path: String::new(),
arch: None,
ssh: None,
tunnel: false,
env: Default::default(),
},
})
.collect();
FullCluster {
controller: config.controller.clone(),
workers,
salt,
env: config.env.clone(),
}
}
type RemoteAgentChild = (String, std::process::Child, Vec<thread::JoinHandle<()>>);
type LocalJoin = (
String,
Option<thread::JoinHandle<Result<Vec<agent::HostChild>>>>,
);
pub struct CoordSpec {
pub backend: crate::distributed::ddp_run::AverageBackend,
pub config_factory: Box<
dyn FnOnce(
usize,
) -> Result<
crate::distributed::cluster_coordinator::ClusterCoordinatorConfig,
> + Send,
>,
}
pub fn run_launcher_with_config(
full: FullCluster,
coord: Option<CoordSpec>,
outer_optimizer: Option<Box<dyn crate::distributed::OuterOptimizer>>,
abort: Arc<std::sync::atomic::AtomicBool>,
) -> Result<()> {
use crate::distributed::membership;
claim_cluster_entry("launcher")?;
let salt = crate::distributed::wire::generate_session_salt();
let full = full.with_session_salt(salt);
let me = crate::distributed::cluster::resolve_hostname()?;
let backend_is_nccl = coord
.as_ref()
.map(|c| matches!(c.backend, crate::distributed::ddp_run::AverageBackend::Nccl))
.unwrap_or(true);
let has_coord = coord.is_some();
let relay_data_channel = has_coord && !backend_is_nccl;
let bind_loopback = validate_tunnel_topology(&full, &me, backend_is_nccl)?;
let mux_port = full.controller.port;
let mux_bind_ip = if bind_loopback { "127.0.0.1" } else { "0.0.0.0" };
let mux_bind = format!("{mux_bind_ip}:{mux_port}");
let mux_listener = std::net::TcpListener::bind(&mux_bind).map_err(|e| {
TensorError::new(&format!(
"cluster launcher: bind {mux_bind} failed: {e}"
))
})?;
let (port_mux, mux_accept) = crate::distributed::port_mux::PortMux::start(
mux_listener,
Arc::clone(&abort),
)?;
let crate::distributed::port_mux::MuxAccept {
rendezvous: mux_rendezvous,
data: mux_data,
control: mux_control,
join: mux_join,
status: mux_status,
} = mux_accept;
eprintln!(
"cluster launcher: port mux bound on {mux_bind_ip}:{} \
(join + rendezvous + data + control + status{})",
port_mux.port(),
if bind_loopback { "; loopback-only, all workers tunneled" } else { "" },
);
let status_board = crate::distributed::status::StatusBoard::new();
let mut status_server = {
let board = status_board.clone();
let source =
crate::distributed::port_mux::StreamSource::Mux(mux_status);
let abort_c = Arc::clone(&abort);
Some(
thread::Builder::new()
.name("flodl-status-http".to_string())
.spawn(move || {
crate::distributed::status::serve_status(
source, board, abort_c,
);
})
.map_err(|e| {
TensorError::new(&format!(
"cluster launcher: spawn status responder failed: {e}"
))
})?,
)
};
let controller_host_cfg = full.controller.host.clone();
let launcher_host = me.clone();
let controller_dial_host = move |worker: &FullWorker| -> String {
if worker.tunnel || (bind_loopback && worker.host == launcher_host) {
"127.0.0.1".to_string()
} else {
controller_host_cfg.clone()
}
};
let capacity = full.world_size();
let join_config = derive_join_config(
full.controller.join.as_ref(),
capacity,
);
let open_admission = membership::resolve_open_admission(&join_config, bind_loopback);
let gate_config = join_config.clone();
let gate_salt = salt;
let gate_abort = Arc::clone(&abort);
let gate_status = status_board.clone();
let gate_source = crate::distributed::port_mux::StreamSource::Mux(mux_join);
let gate = thread::Builder::new()
.name("flodl-join-gate".to_string())
.spawn(move || {
membership::run_join_window(
&gate_source,
&gate_config,
&gate_salt,
!open_admission,
None,
&gate_abort,
&gate_status,
)
})
.map_err(|e| {
TensorError::new(&format!(
"cluster launcher: spawn join-gate thread failed: {e}"
))
})?;
let has_remote = full
.workers
.iter()
.any(|h| h.host != me && !h.ranks.is_empty());
let fdl_cmd = if has_remote {
Some(env::var(ENV_FDL_CMD).map_err(|_| {
TensorError::new(&format!(
"cluster launcher: topology has remote hosts but {ENV_FDL_CMD} \
is not set in env. fdl-cli must export the fdl command name \
(e.g. {ENV_FDL_CMD}=train) when invoking the launcher."
))
})?)
} else {
None
};
let overlay_env = env::var(ENV_FDL_ENV).ok().filter(|s| !s.trim().is_empty());
let prebuild_envelope = load_prebuild_envelope()?;
let remote_cleanup_targets: Vec<(FullWorker, String)> = full
.workers
.iter()
.filter(|h| h.host != me)
.filter_map(|h| {
prebuild_envelope.get(&h.host).map(|pb| {
let abs_bin = format!(
"{}/{}",
h.path.trim_end_matches('/'),
pb.bin,
);
(h.clone(), abs_bin)
})
})
.collect();
cleanup_remote_hosts_parallel(remote_cleanup_targets.clone());
let mut remote_agents: Vec<RemoteAgentChild> = Vec::new();
let mut local_joins: Vec<LocalJoin> = Vec::new();
let salt_hex_for_agents = (!open_admission)
.then(|| crate::distributed::wire::salt_to_hex(&salt));
let spawn_result: Result<()> = (|| {
for host in &full.workers {
if host.ranks.is_empty() {
continue;
}
let spec = agent::AgentSpec {
host: host.host.clone(),
controller_host: if host.host == me {
"127.0.0.1".to_string()
} else {
controller_dial_host(host)
},
controller_port: mux_port,
salt_hex: salt_hex_for_agents.clone(),
local_devices: host.local_devices.clone(),
libtorch: host.arch.clone().unwrap_or_default(),
dataset_sig_hex: None,
};
if host.host == me {
let mut extra_env = full.env.clone();
extra_env.extend(host.env.clone());
let host_name = host.host.clone();
let handle = thread::Builder::new()
.name(format!("flodl-local-join:{host_name}"))
.spawn(move || agent::join_and_spawn_local(spec, &extra_env))
.map_err(|e| {
TensorError::new(&format!(
"cluster launcher: spawn local join thread failed: {e}"
))
})?;
local_joins.push((host.host.clone(), Some(handle)));
} else {
let spec_hex = spec.to_env_hex()?;
let remote_cmd = build_remote_agent_bash_command(
&host.path,
&host.host,
overlay_env.as_deref(),
fdl_cmd
.as_deref()
.expect("ENV_FDL_CMD presence enforced above when has_remote"),
&env::args().skip(1).collect::<Vec<String>>(),
&full.env,
&host.env,
prebuild_envelope.get(&host.host),
);
let mut cmd = build_ssh_spawn_command(
host,
&remote_cmd,
host.tunnel.then_some(mux_port),
);
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().map_err(|e| {
TensorError::new(&format!(
"cluster launcher: spawn ssh agent for {:?} failed: {e}",
host.host
))
})?;
spawn::pipe_envelope_to_child(&mut child, &spec_hex);
let mut forwarders = Vec::with_capacity(2);
if let Some(out) = child.stdout.take() {
forwarders.push(thread::spawn(move || {
forward_lines(out, String::new(), false);
}));
}
if let Some(err) = child.stderr.take() {
forwarders.push(thread::spawn(move || {
forward_lines(err, String::new(), true);
}));
}
remote_agents.push((host.host.clone(), child, forwarders));
}
}
Ok(())
})();
let teardown_early = |remote_agents: &mut Vec<RemoteAgentChild>,
local_joins: &mut Vec<LocalJoin>| {
abort.store(true, std::sync::atomic::Ordering::SeqCst);
for (_, child, forwarders) in remote_agents.drain(..) {
let mut child = child;
let _ = child.kill();
let _ = child.wait();
for f in forwarders {
let _ = f.join();
}
}
for (_, handle) in local_joins.iter_mut() {
if let Some(h) = handle.take() {
if let Ok(Ok(children)) = h.join() {
for mut c in children {
let _ = c.child.kill();
let _ = c.child.wait();
for f in c.forwarders {
let _ = f.join();
}
}
}
}
}
cleanup_remote_hosts_parallel(remote_cleanup_targets.clone());
};
if let Err(e) = spawn_result {
eprintln!(
"cluster launcher: fan-out failed; tearing down {} agent(s): {e}",
remote_agents.len() + local_joins.len(),
);
teardown_early(&mut remote_agents, &mut local_joins);
let _ = gate.join();
if let Some(h) = status_server.take() {
let _ = h.join();
}
return Err(e);
}
let mut dead_agent: Option<String> = None;
let formed = loop {
if gate.is_finished() {
break gate.join().map_err(|_| {
TensorError::new("cluster launcher: join-gate thread panicked")
})?;
}
if dead_agent.is_none() {
for (host, child, _) in remote_agents.iter_mut() {
if let Ok(Some(st)) = child.try_wait() {
eprintln!(
"cluster launcher: agent of {host:?} exited with \
{st} before the world formed; aborting the window"
);
dead_agent = Some(host.clone());
abort.store(true, std::sync::atomic::Ordering::SeqCst);
break;
}
}
}
thread::sleep(std::time::Duration::from_millis(20));
};
let formed = match formed {
Ok(f) => f,
Err(e) => {
teardown_early(&mut remote_agents, &mut local_joins);
if let Some(h) = status_server.take() {
let _ = h.join();
}
return Err(match dead_agent {
Some(host) => TensorError::new(&format!(
"cluster launcher: agent of {host:?} died before the world \
formed ({e})"
)),
None => e,
});
}
};
let membership::FormedWorld {
workers: formed_workers,
world_size,
snapshot: mut membership_state,
} = formed;
let world = synthesize_world(
&full,
formed_workers.iter().map(|aw| &aw.member),
salt,
);
let my_host_idx = world.workers.iter().position(|h| h.host == me);
let mut coord_config = match coord {
Some(spec) => Some((spec.config_factory)(world_size)?),
None => None,
};
let elastic_max_failure = coord_config.as_ref().and_then(|c| c.max_failure);
let dead_ranks_shared =
crate::distributed::controller::DeadRanks::new(world_size);
let model_schema = coord_config
.as_mut()
.and_then(|c| c.model_schema.take());
let checkpoint_forge =
crate::distributed::CheckpointForge::new(model_schema);
if let Some(cfg) = coord_config.as_mut() {
cfg.checkpoint_forge = Some(Arc::clone(&checkpoint_forge));
}
let cpu_averager =
crate::distributed::controller::ClusterController::start_from_source(
crate::distributed::port_mux::StreamSource::Mux(mux_data),
mux_port,
world_size,
salt,
Arc::clone(&dead_ranks_shared),
Some(Arc::clone(&checkpoint_forge)),
outer_optimizer,
)?;
eprintln!(
"cluster launcher: ClusterController up on port {} (world_size={})",
cpu_averager.port(),
world_size,
);
let mut dashboard_sink_outer:
Option<Arc<dyn crate::distributed::DashboardSink>> = None;
let reported_deaths: crate::distributed::cluster_coordinator::ReportedDeaths =
Arc::new(std::sync::Mutex::new(Vec::new()));
let mut coord_driver: Option<thread::JoinHandle<()>> = None;
let mut rdv_driver: Option<thread::JoinHandle<()>> = None;
let coord_fatal: Arc<std::sync::Mutex<Option<String>>> =
Arc::new(std::sync::Mutex::new(None));
let rank_resources = coord_config
.as_ref()
.is_some_and(|c| c.timeline.is_some());
if let Some(mut config) = coord_config {
use crate::distributed::cluster_coordinator::ClusterCoordinator;
let local_ranks: Vec<usize> = my_host_idx
.map(|i| world.workers[i].ranks.clone())
.unwrap_or_default();
let dead_ranks = Arc::clone(&dead_ranks_shared);
let mut rank_hosts: Vec<String> = vec![String::new(); world_size];
for w in &world.workers {
for &r in &w.ranks {
if let Some(slot) = rank_hosts.get_mut(r) {
*slot = w.host.clone();
}
}
}
config = config
.local_ranks(local_ranks.clone())
.rank_hosts(rank_hosts)
.dead_ranks(dead_ranks)
.reported_deaths(Arc::clone(&reported_deaths));
if let (Some(tl), Some(idx)) = (&config.timeline, my_host_idx) {
tl.set_host(&world.workers[idx].host);
}
if local_ranks.is_empty()
&& let Some(tl) = &config.timeline
{
tl.set_gpu_poll(false);
}
let record_log = config.record_log_dir.as_ref().map(|dir| {
Arc::new(crate::monitor::record_log::RecordLog::new(
dir,
config
.max_log_size
.unwrap_or(crate::monitor::record_log::DEFAULT_MAX_LOG_BYTES),
))
});
let dashboard_sink: Arc<dyn crate::distributed::DashboardSink> =
Arc::new(crate::distributed::ClusterDashboardSink::new(
Arc::new(world.clone()),
me.clone(),
config.num_epochs,
)
.with_record_log(record_log)
.with_scalar_reductions(config.scalar_reductions.clone())
.with_dashboard_html(config.dashboard_html.clone())
.with_dashboard_theme(config.dashboard_theme.clone()));
dashboard_sink_outer = Some(Arc::clone(&dashboard_sink));
config = config.dashboard_sink(Arc::clone(&dashboard_sink));
let coord_salt = salt;
eprintln!(
"cluster launcher: ClusterCoordinator spawning on port {} \
(world_size={}, local_ranks={:?})",
mux_port, world_size, local_ranks,
);
let start_epoch = config.start_epoch;
config = config.abort_flag(Arc::clone(&abort));
let coord_abort = Arc::clone(&abort);
let coord_fatal_slot = Arc::clone(&coord_fatal);
let coord_source =
crate::distributed::port_mux::StreamSource::Mux(mux_control);
coord_driver = Some(thread::Builder::new()
.name("flodl-cluster-coord".to_string())
.spawn(move || {
match ClusterCoordinator::start_from_source(
coord_source, mux_port, coord_salt, config,
) {
Ok(mut coord) => {
let kicked = match coord.resume_progressive_from_coverage() {
Ok(handled) => handled,
Err(e) => {
eprintln!(
"cluster launcher: resume_progressive_from_coverage failed: {e}"
);
return;
}
};
if !kicked {
if let Err(e) = coord.dispatch_epoch(start_epoch) {
eprintln!(
"cluster launcher: dispatch_epoch({start_epoch}) failed: {e}"
);
return;
}
}
loop {
if coord_abort.load(std::sync::atomic::Ordering::SeqCst) {
break;
}
coord.drain_timing_blocking(
std::time::Duration::from_millis(2),
);
match coord.tick() {
Ok(true) => continue,
Ok(false) => break,
Err(e) => {
eprintln!(
"cluster launcher: coord tick error: {e}"
);
break;
}
}
}
}
Err(e) => {
eprintln!(
"cluster launcher: ClusterCoordinator start failed: {e}"
);
if let Ok(mut slot) = coord_fatal_slot.lock() {
*slot = Some(e.to_string());
}
coord_abort.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
})
.map_err(|e| {
TensorError::new(&format!(
"cluster launcher: spawn coord thread failed: {e}"
))
})?);
} else {
drop(mux_control);
}
let cohort_formed = Arc::new(std::sync::atomic::AtomicBool::new(!backend_is_nccl));
if backend_is_nccl {
let rdv_full = world.clone();
let rdv_me = me.clone();
let formed_for_rdv = Arc::clone(&cohort_formed);
let rdv_abort = Arc::clone(&abort);
let rdv_source =
crate::distributed::port_mux::StreamSource::Mux(mux_rendezvous);
rdv_driver = Some(thread::Builder::new()
.name("flodl-cluster-rendezvous".to_string())
.spawn(move || {
match crate::distributed::rendezvous::run_controller_rendezvous_aborting(
&rdv_full, &rdv_me, rdv_source, &rdv_abort,
) {
Ok(()) => formed_for_rdv.store(true, std::sync::atomic::Ordering::SeqCst),
Err(e) => {
eprintln!("cluster launcher: rendezvous server error: {e}");
}
}
})
.map_err(|e| {
TensorError::new(&format!(
"cluster launcher: spawn rendezvous thread failed: {e}"
))
})?);
} else {
drop(mux_rendezvous);
}
let _ = COHORT_INVENTORY.set(
formed_workers.iter().map(|aw| aw.member.clone()).collect(),
);
let frame_ceiling = crate::distributed::wire::frame_ceiling();
let mut rank_exit_readers: Vec<thread::JoinHandle<()>> = Vec::new();
for (idx, aw) in formed_workers.into_iter().enumerate() {
let worker = &world.workers[idx];
let member = aw.member;
let mut stream = aw.stream;
let dial_host = controller_dial_host(worker);
let envelope = build_slim_envelope_for(&world, worker, &dial_host, rank_resources);
let envelope_hex = crate::distributed::cluster::hex_encode(
serde_json::to_string(&envelope)
.map_err(|e| {
TensorError::new(&format!(
"cluster launcher: serialize slim envelope failed: {e}"
))
})?
.as_bytes(),
);
let relay_spec_hex = if has_coord {
let spec = RelaySpec {
host: member.host.clone(),
controller_host: dial_host,
controller_port: mux_port,
ranks: member.ranks.iter().map(|r| *r as u32).collect(),
salt_hex: crate::distributed::wire::salt_to_hex(&salt),
world_size,
data_channel: relay_data_channel,
frame_ceiling_bytes: frame_ceiling,
};
Some(crate::distributed::cluster::hex_encode(
serde_json::to_string(&spec)
.map_err(|e| {
TensorError::new(&format!(
"cluster launcher: serialize relay spec failed: {e}"
))
})?
.as_bytes(),
))
} else {
None
};
let msg = crate::distributed::wire::JoinMsgWire::WorldFormed {
envelope_hex,
relay_spec_hex,
};
let send = crate::distributed::wire::ControlFrame::encode(
&salt,
crate::distributed::wire::MsgKind::Join,
&msg,
)
.and_then(|f| f.write_to(&mut stream));
if let Err(e) = send {
eprintln!(
"cluster launcher: WorldFormed to {:?} failed ({e}); \
reporting its rank(s) {:?} dead",
member.host, member.ranks,
);
if let Ok(mut q) = reported_deaths.lock() {
q.extend(member.ranks.iter().copied());
}
continue;
}
let reader_deaths = Arc::clone(&reported_deaths);
let reader_salt = salt;
let reader_host = member.host.clone();
rank_exit_readers.push(thread::spawn(move || {
let _ = stream.set_read_timeout(None);
loop {
match crate::distributed::wire::ControlFrame::read_from(
&mut stream,
&reader_salt,
) {
Ok(Some(frame)) => {
match frame.decode::<crate::distributed::wire::JoinMsgWire>() {
Ok(crate::distributed::wire::JoinMsgWire::RankExited {
rank,
code,
}) if code != 0 => {
eprintln!(
"cluster launcher: host {reader_host:?} reports \
rank {rank} exited with code {code}; feeding \
elastic membership"
);
if let Ok(mut q) = reader_deaths.lock() {
q.push(rank as usize);
}
}
Ok(_) => {}
Err(e) => {
crate::verbose!(
" cluster launcher: control-link decode from \
{reader_host:?}: {e}"
);
}
}
}
Ok(None) | Err(_) => return,
}
}
}));
}
let ranks_by_host: std::collections::BTreeMap<String, Vec<usize>> = world
.workers
.iter()
.map(|w| (w.host.clone(), w.ranks.clone()))
.collect();
let mut children: Vec<spawn::SupervisedChild> = Vec::new();
let mut collect_err: Option<TensorError> = None;
for (host, handle) in local_joins.iter_mut() {
let joined = handle
.take()
.expect("local join handle consumed once")
.join();
match joined {
Ok(Ok(host_children)) => {
let host_ranks = ranks_by_host.get(host).cloned().unwrap_or_default();
for hc in host_children {
let granks = match hc.rank {
Some(r) => vec![r as usize],
None => host_ranks.clone(),
};
children.push((host.clone(), hc.slot, granks, hc.child, hc.forwarders));
}
}
Ok(Err(e)) => {
collect_err.get_or_insert_with(|| {
TensorError::new(&format!(
"cluster launcher: local worker {host:?} failed to spawn \
its children: {e}"
))
});
}
Err(_) => {
collect_err.get_or_insert_with(|| {
TensorError::new(&format!(
"cluster launcher: local join thread for {host:?} panicked"
))
});
}
}
}
for (host, child, forwarders) in remote_agents.drain(..) {
let granks = ranks_by_host.get(&host).cloned().unwrap_or_default();
children.push((host, AGENT_RANK_SENTINEL, granks, child, forwarders));
}
if let Some(e) = collect_err {
eprintln!("{e}");
for (_, _, _, mut child, forwarders) in children.drain(..) {
let _ = child.kill();
let _ = child.wait();
for f in forwarders {
let _ = f.join();
}
}
cleanup_remote_hosts_parallel(remote_cleanup_targets.clone());
abort.store(true, std::sync::atomic::Ordering::SeqCst);
if let Some(h) = coord_driver.take() {
let _ = h.join();
}
if let Some(h) = rdv_driver.take() {
let _ = h.join();
}
for r in rank_exit_readers {
let _ = r.join();
}
if let Some(h) = status_server.take() {
let _ = h.join();
}
return Err(e);
}
membership_state.phase = membership::ClusterPhase::Training;
status_board.publish(&membership_state);
let any_failure = supervise_children(
children,
has_coord.then(|| ElasticSupervision {
reported_deaths: Arc::clone(&reported_deaths),
dead_ranks: Arc::clone(&dead_ranks_shared),
max_failure: elastic_max_failure,
world_size,
cohort_formed: Arc::clone(&cohort_formed),
}),
Some(Arc::clone(&abort)),
);
let any_failure = match coord_fatal.lock().ok().and_then(|mut s| s.take()) {
Some(root) => Some(TensorError::new(&format!(
"cluster launcher: coordinator failed at formation — {root}"
))),
None => any_failure,
};
membership_state.phase = if any_failure.is_some() {
membership::ClusterPhase::Failed
} else {
membership::ClusterPhase::Done
};
status_board.publish(&membership_state);
cleanup_remote_hosts_parallel(remote_cleanup_targets);
if let Some(ref sink) = dashboard_sink_outer {
sink.shutdown();
}
if let Err(e) = cpu_averager.shutdown() {
eprintln!("cluster launcher: ClusterController shutdown failed: {e}");
}
abort.store(true, std::sync::atomic::Ordering::SeqCst);
if let Some(h) = coord_driver.take() {
let _ = h.join();
}
if let Some(h) = rdv_driver.take() {
let _ = h.join();
}
for r in rank_exit_readers {
let _ = r.join();
}
if let Some(h) = status_server.take() {
let _ = h.join();
}
use std::io::Write as _;
let _ = std::io::stderr().flush();
let _ = std::io::stdout().flush();
if let Some(err) = any_failure {
eprintln!("cluster launcher: fatal failure: {err}");
let _ = std::io::stderr().flush();
return Err(err);
}
Ok(())
}