1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
//! Lifecycle methods for [`super::ClusterCoordinator`]: bind, accept,
//! shutdown, and the outbound control-frame I/O.
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, mpsc};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use crate::distributed::ddp_run::ApplyPolicy;
use crate::distributed::relay::mux::{MuxRecord, RelayControlMsg};
use crate::distributed::wire::{ControlFrame, ControlMsgWire, MsgKind, SessionSalt, TimingMsgWire};
use crate::tensor::{Result, TensorError};
use super::{
ClusterCoordinator, ClusterCoordinatorConfig,
RunPhase, initial_callback_role, relay_reader_loop,
};
impl ClusterCoordinator {
/// Bind a control TcpListener at `bind_addr`, accept exactly
/// `world_size` rank connections (validating the session salt at
/// handshake), spawn per-rank reader threads, and return the
/// configured coordinator.
///
/// Returns `Err` if any handshake fails (loud error: salt mismatch,
/// magic mismatch, version mismatch, world_size disagreement,
/// duplicate rank_id).
pub fn start(
bind_addr: SocketAddr,
salt: SessionSalt,
config: ClusterCoordinatorConfig,
) -> Result<Self> {
let (listener, _port) = Self::bind(bind_addr)?;
Self::start_from_listener(listener, salt, config)
}
/// Bind the control listener without blocking on accept. Useful for
/// tests that need to publish the bound port before spawning rank
/// connections (the post-bind accept loop blocks the calling
/// thread until every rank has connected).
pub fn bind(bind_addr: SocketAddr) -> Result<(TcpListener, u16)> {
let listener = TcpListener::bind(bind_addr).map_err(|e| {
TensorError::new(&format!(
"cluster_coordinator: bind {bind_addr} failed: {e}"
))
})?;
let bound_port = listener
.local_addr()
.map_err(|e| {
TensorError::new(&format!(
"cluster_coordinator: local_addr() failed: {e}"
))
})?
.port();
Ok((listener, bound_port))
}
/// Accept connections + handshake on a pre-bound listener. Pair
/// with [`Self::bind`] when the caller needs the port before
/// blocking on accepts (e.g. tests that spawn rank threads after
/// publishing the port through a channel).
pub fn start_from_listener(
listener: TcpListener,
salt: SessionSalt,
config: ClusterCoordinatorConfig,
) -> Result<Self> {
let bound_port = listener
.local_addr()
.map_err(|e| {
TensorError::new(&format!(
"cluster_coordinator: local_addr() failed: {e}"
))
})?
.port();
let source = crate::distributed::port_mux::StreamSource::from_listener(
listener,
"cluster_coordinator",
)?;
Self::start_from_source(source, bound_port, salt, config)
}
/// Like [`Self::start_from_listener`] but accepting connections from
/// a pre-built [`StreamSource`] — the production entry, handed the
/// control leg of the launcher's single-port mux. `bound_port` is
/// carried for diagnostics ([`Self::bound_port`]).
///
/// [`StreamSource`]: crate::distributed::port_mux::StreamSource
pub(crate) fn start_from_source(
source: crate::distributed::port_mux::StreamSource,
bound_port: u16,
salt: SessionSalt,
config: ClusterCoordinatorConfig,
) -> Result<Self> {
let world_size = config.world_size;
if world_size == 0 {
return Err(TensorError::new(
"cluster_coordinator: world_size must be > 0",
));
}
// Accept per-host relay connections, each announcing the ranks it
// carries via a `RelayHello`. Accept until every global rank is
// covered exactly once. `control_streams` holds the write half per
// connection (the coord is the sole writer); `rank_to_conn` maps a
// rank to its owning connection for `send_control`. Each
// connection's read half goes to a per-host reader thread that
// demuxes by rank tag.
let mut control_streams: Vec<TcpStream> = Vec::new();
let mut rank_to_conn: Vec<Option<usize>> = (0..world_size).map(|_| None).collect();
let mut conn_reads: Vec<TcpStream> = Vec::new();
let mut covered = 0usize;
// Poll accept instead of blocking: on a pre-rendezvous failure the
// relays never dial in, and the launcher must be able to stop this
// loop (via `ClusterCoordinatorConfig::abort`), join the coord
// thread, and surface the original error through
// `DdpHandle::join`. The formation deadline is the self-contained
// backstop for the same scenario when no abort arrives: the join
// window bounded admission, this bounds the dial-in that follows —
// a registered-but-never-dialing relay must fail the run loudly
// here, not leave the cohort to die of its own rank-side deadlines.
let formation_deadline = std::time::Instant::now()
+ Duration::from_secs(config.formation_timeout_secs);
while covered < world_size {
let mut stream = match source.try_accept("cluster_coordinator")? {
Some(s) => s,
None => {
if config
.abort
.as_ref()
.is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst))
{
return Err(TensorError::new(&format!(
"cluster_coordinator: aborted by launcher before \
cohort formed ({covered}/{world_size} ranks covered)"
)));
}
if std::time::Instant::now() >= formation_deadline {
return Err(TensorError::new(&format!(
"cluster_coordinator: relay dial-in did not cover the \
world within {}s ({covered}/{world_size} ranks \
covered) — a relay failed to start or dial; aborting \
formation (FLODL_NET_TIMEOUT_SCALE scales this \
deadline)",
config.formation_timeout_secs,
)));
}
std::thread::sleep(Duration::from_millis(10));
continue;
}
};
let _ = stream.set_nodelay(true);
// 10s handshake timeout protects against a wedged relay.
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.map_err(|e| {
TensorError::new(&format!("cluster_coordinator: set_read_timeout: {e}"))
})?;
// Channel-select magic, then the relay handshake.
crate::distributed::wire::expect_channel_magic(
&mut stream,
crate::distributed::wire::CHANNEL_MAGIC_CONTROL,
"cluster_coordinator",
)?;
let ranks = match MuxRecord::read_from(&mut stream, &salt)? {
Some(MuxRecord::Control(RelayControlMsg::Hello { host, ranks })) => {
crate::verbose!(
" cluster_coordinator: relay '{host}' carries ranks {ranks:?}"
);
ranks
}
Some(other) => {
return Err(TensorError::new(&format!(
"cluster_coordinator: expected relay Hello, got {other:?}"
)));
}
None => {
return Err(TensorError::new(
"cluster_coordinator: relay closed connection before Hello",
));
}
};
let conn_idx = control_streams.len();
for r in &ranks {
let r = *r as usize;
if r >= world_size {
return Err(TensorError::new(&format!(
"cluster_coordinator: relay announced rank {r} >= world_size {world_size}"
)));
}
if rank_to_conn[r].is_some() {
return Err(TensorError::new(&format!(
"cluster_coordinator: rank {r} announced by two relays"
)));
}
rank_to_conn[r] = Some(conn_idx);
covered += 1;
}
MuxRecord::control(RelayControlMsg::HelloAck).write_to(&mut stream, &salt)?;
// Reader holds a try-cloned read half (short timeout so it can
// observe shutdown between records); the coord keeps the write
// half for `send_control`.
let read_half = stream.try_clone().map_err(|e| {
TensorError::new(&format!("cluster_coordinator: relay try_clone: {e}"))
})?;
read_half
.set_read_timeout(Some(Duration::from_millis(250)))
.map_err(|e| {
TensorError::new(&format!(
"cluster_coordinator: reader set_read_timeout: {e}"
))
})?;
// Write-stall ceiling on the socket (fd-level, covers the
// retained write half): a wedged relay must error the tick
// thread's send instead of parking it forever — the tick
// thread also drives the dead-rank detector that would
// otherwise rescue the situation.
stream
.set_write_timeout(Some(crate::distributed::wire::write_stall_timeout()))
.map_err(|e| {
TensorError::new(&format!(
"cluster_coordinator: set_write_timeout: {e}"
))
})?;
control_streams.push(stream);
conn_reads.push(read_half);
}
// Spawn one reader thread per relay connection; all feed the same
// timing / metrics channels (the rank rides in each frame's mux
// tag + payload).
let shutdown_flag = Arc::new(AtomicBool::new(false));
let (timing_tx, timing_rx) = mpsc::channel::<TimingMsgWire>();
let (metrics_tx, metrics_rx) =
mpsc::channel::<crate::distributed::wire::MetricsMsgWire>();
let mut reader_handles: Vec<Option<JoinHandle<()>>> =
Vec::with_capacity(conn_reads.len());
for (conn_idx, mut read_half) in conn_reads.into_iter().enumerate() {
let tx = timing_tx.clone();
let mtx = metrics_tx.clone();
let salt_for_reader = salt;
let shutdown_for_reader = Arc::clone(&shutdown_flag);
let spawn_result = thread::Builder::new()
.name(format!("flodl-coord-relay{conn_idx}"))
.spawn(move || {
relay_reader_loop(
&mut read_half,
&salt_for_reader,
&shutdown_for_reader,
&tx,
&mtx,
);
});
match spawn_result {
Ok(handle) => reader_handles.push(Some(handle)),
Err(e) => {
// Partial-init cleanup: a mid-loop spawn failure (OS
// thread limit) must not leak the readers already
// running. Signal them to stop and join before
// returning — without this they'd outlive the aborted
// bootstrap, blocked on their sockets (the control
// write halves drop on return, but the readers hold
// their own try_cloned read halves and only observe
// shutdown via the flag, on their 250ms poll).
shutdown_flag.store(true, Ordering::SeqCst);
for h in reader_handles.iter_mut() {
if let Some(j) = h.take() {
let _ = j.join();
}
}
return Err(TensorError::new(&format!(
"cluster_coordinator: spawn relay reader {conn_idx}: {e}"
)));
}
}
}
// Drop the extra senders we cloned for the closures; loop exit
// depends on every cloned sender being dropped, but that happens
// automatically when reader threads exit.
drop(timing_tx);
drop(metrics_tx);
let streams = control_streams;
// Resume: layer saved trajectory state on top of the user-built
// ElChe (which carries the user's knobs from this run's
// DdpRunConfig). When `start_elche_state` is None, the ElChe
// stays fresh.
let mut el_che = config.el_che;
if let Some(ref state) = config.start_elche_state {
el_che.restore_from_state(state)?;
}
// Cap the reduce window to one epoch's batches (coverage-global).
// The overhead auto-tune may grow the schedule to amortize an
// expensive sync, but a window must never span more than one
// dataset pass — otherwise syncs collapse to <1/epoch (observed:
// CPU cadence grew the window to 1092 batches against a 781-batch
// epoch, dropping to ~1 sync/epoch and serializing the cohort).
// No-op for NCCL (cheap sync → window stays well under) and for
// any run where the epoch size isn't known yet.
if config.batch_size > 0 && config.total_samples >= config.batch_size {
el_che.set_max_total_batches(config.total_samples / config.batch_size);
}
// `calibrated` mirrors the post-restore ElChe state: true when
// any rank has a positive smoothed reading. Matches the
// invariant the snapshot was taken under.
let calibrated = config.start_elche_state.is_some()
&& el_che.is_calibrated();
Ok(ClusterCoordinator {
policy: config.policy,
backend: config.backend,
world_size,
overshoot_initial: config.overshoot_initial,
overshoot_ceiling: config.overshoot_ceiling,
overshoot_auto: config.overshoot_auto,
elche_relax_up: config.elche_relax_up,
el_che,
convergence_guard: config.convergence_guard,
version: 0,
avg_count: config.start_avg_count,
global_step: config.start_global_step,
calibrated,
active_count: world_size,
max_overshoot: config.overshoot_initial,
window: super::window_ledger::WindowLedger::new(world_size),
last_batch_ms: vec![0.0; world_size],
last_step_count: vec![0; world_size],
cycle: super::cycle_state::AvgCycleState::new(config.backend, world_size),
dispatch_hold_logged: vec![false; world_size],
// Per-epoch d-aggregator identity values; see field docs on
// `ClusterCoordinator`.
epoch_d_min: f64::INFINITY,
epoch_d_max: f64::NEG_INFINITY,
epoch_d_sum: 0.0,
epoch_d_count: 0,
epoch_last_d: 0.0,
epoch_last_k_max: 0,
lr_event_meta: if config.meta_controller {
Some(crate::distributed::lr_event_meta::LrEventMeta::with_default_config())
} else {
None
},
last_lr_per_rank: vec![None; world_size],
lost_broadcasts: 0,
prof_enabled: crate::log::enabled(crate::log::Verbosity::Debug),
stall_last_global_step: 0,
stall_since: None,
stall_last_dump: None,
dead_ranks: config.dead_ranks,
reported_deaths: config.reported_deaths,
heartbeat_timeout_secs: config.heartbeat_timeout_secs,
rendezvous_timeout_secs: config.rendezvous_timeout_secs,
last_heartbeat: vec![Instant::now(); world_size],
last_coord_heartbeat: None,
exited: vec![false; world_size],
last_step_count_at_epoch_start: vec![0; world_size],
nccl_rendezvous_pending: None,
local_ranks: config.local_ranks.clone(),
rank_hosts: config.rank_hosts.clone(),
max_failure: config.max_failure,
epoch_callback_policy: config.epoch_callback_policy,
checkpoint_role: initial_callback_role(
config.epoch_callback_policy,
world_size,
),
eval_role: initial_callback_role(
config.epoch_callback_policy,
world_size,
),
pending_eval_intent: false,
pending_checkpoint_intent: false,
epoch_callback_role: initial_callback_role(
config.epoch_callback_policy,
world_size,
),
epoch_role_dirty: true,
checkpoint_tried_ranks: std::collections::HashMap::new(),
last_checkpoint_elapsed_ms_ewma: None,
last_eval_elapsed_ms_ewma: None,
last_epoch_fn_elapsed_ms_ewma: None,
save_path: config.save_path.clone(),
checkpoint_every: config.checkpoint_every,
seed: config.seed,
checkpoint_at_epoch: config.checkpoint_at_epoch,
start_coverage: config.start_coverage.clone(),
checkpoint_forge: config.checkpoint_forge.clone(),
pending_checkpoint_coverage: None,
shutdown_with_save_dispatched: false,
rank_epoch: vec![0; world_size],
last_aggregated_epoch: None,
last_dispatched_epoch: None,
run_phase: RunPhase::Training,
epoch_plan_cache: std::collections::HashMap::new(),
total_samples: config.total_samples,
batch_size: config.batch_size.max(1),
num_epochs: config.num_epochs,
partition_ratios: config.partition_ratios,
timing_rx,
metrics_rx,
metrics_buffer: std::collections::BTreeMap::new(),
chunk_pools: std::collections::BTreeMap::new(),
// Resolve progressive: explicit override wins, otherwise
// auto-on for Cadence/Async, off for Sync (Sync dispatches
// whole epochs; there is nothing to stream).
progressive: config.progressive.unwrap_or(
!matches!(config.policy, ApplyPolicy::Sync),
),
// Floor for proportional chunk sizing after calibration.
min_chunk_batches: 4,
final_window_plan: None,
metrics_fn: config.metrics_fn.clone(),
metrics_sink_tx: config.metrics_sink_tx.clone(),
eval_result_fn: config.eval_result_fn.clone(),
eval_every_epochs: config.eval_every_epochs,
// Sub-epoch report cadence. The scheduler's `epoch_work` is
// steps-per-epoch — known ahead from the dataset, so the report
// interval is a pure function of config, not of observed
// progress. `report_interval(.., 1)` IS steps-per-epoch (one
// report per epoch = one interval), and carries the degenerate
// guard: a zero batch_size yields a non-finite work, which the
// scheduler treats as "never fire".
report_scheduler: config.reports_per_epoch.map(|x| {
let steps_per_epoch = crate::monitor::cadence::report_interval(
config.total_samples,
config.batch_size,
1,
);
crate::monitor::cadence::ReportScheduler::new(x, steps_per_epoch)
}),
report_in_epoch_steps: 0.0,
report_epoch_seen: 0,
metrics_device_indices: (0..world_size as u8).collect(),
control_streams: streams,
rank_to_conn,
reader_handles,
shutdown_flag,
bound_port,
salt,
timeline: config.timeline.clone(),
sync_start: None,
dashboard_sink: config.dashboard_sink.clone(),
latest_res: vec![crate::monitor::record::ResAcc::default(); world_size],
event_lane: crate::monitor::event_lane::EventLane::new(),
})
}
// -----------------------------------------------------------------
// Outbound control frame I/O
// -----------------------------------------------------------------
pub(super) fn send_control(&mut self, rank: usize, msg: &ControlMsgWire) -> Result<()> {
if rank >= self.world_size {
return Err(TensorError::new(&format!(
"cluster_coordinator: send_control rank {rank} >= world_size {}",
self.world_size
)));
}
// Resolve the relay connection carrying this rank. Unmapped means a
// headless coord (test fixtures via `for_test`, no streams) or a
// rank no relay announced. Return Err so callers that tolerate
// transient send failures (e.g. `handle_checkpoint_result`'s
// retry-dispatch path) log + continue rather than panic.
let Some(conn_idx) = self.rank_to_conn.get(rank).copied().flatten() else {
return Err(TensorError::new(&format!(
"cluster_coordinator: send_control(rank={rank}): no relay connection \
(headless coord, or rank not announced by any relay)"
)));
};
// Encode the control frame, then wrap it as a rank-tagged mux
// record on the per-host connection (the relay demuxes it to the
// local rank).
let frame = ControlFrame::encode(&self.salt, MsgKind::Control, msg)?;
let mut buf = Vec::new();
frame.write_to(&mut buf)?;
MuxRecord::data(rank as u32, buf)
.write_to(&mut self.control_streams[conn_idx], &self.salt)
.map_err(|e| {
TensorError::new(&format!(
"cluster_coordinator: send_control(rank={rank}): {e}"
))
})?;
Ok(())
}
/// Broadcast `msg` to every rank.
///
/// BEST-EFFORT, NOT FAIL-FAST: every rank gets a send attempt even
/// when an earlier one fails. A fail-fast `?` here left every rank
/// AFTER the broken connection unsignaled — a Shutdown that never
/// reached the trailing ranks parked them forever, and a partial
/// RequestParams sent part of the cohort into a reduce barrier its
/// peers never entered.
///
/// Declared-dead ranks are STILL attempted ("dead" often means
/// slow/stale-heartbeat, and frames like ShutdownWithSave exist
/// precisely to reach them) but their failures are expected and only
/// logged at verbose. Returns `Err` listing the LIVE ranks that
/// failed, AFTER attempting all of them; callers on non-abortable
/// paths log it (a live rank that fails has a broken connection, so
/// heartbeat staleness reaps it shortly).
pub(super) fn broadcast_control(&mut self, msg: &ControlMsgWire) -> Result<()> {
let mut failed: Vec<String> = Vec::new();
for rank in 0..self.world_size {
// A rank that latched a clean `Exiting` has left by protocol:
// its relay is closing or closed, so a write either breaks
// (broken pipe) or lands in a buffer nobody will drain. Skip
// it — end-of-run broadcasts (the final `EvalBroadcast`
// racing worker exit was the observed case) otherwise
// manufacture failure reports out of a normal teardown.
if self.exited[rank] {
continue;
}
let dead = self.is_dead(rank);
if let Err(e) = self.send_control(rank, msg) {
if dead {
crate::verbose!(
" ddp: broadcast to declared-dead rank {rank} failed \
(expected): {e}"
);
} else {
failed.push(format!("rank {rank}: {e}"));
}
}
}
if failed.is_empty() {
Ok(())
} else {
// Structured trace of a dropped best-effort broadcast (see
// `alerts_lost_broadcast` for which drops are worth alerting on).
if alerts_lost_broadcast(msg, self.run_phase) {
self.note_lost_broadcast(control_label(msg), failed.len());
}
Err(TensorError::new(&format!(
"cluster_coordinator: broadcast_control failed for {} of {} ranks [{}]",
failed.len(),
self.world_size,
failed.join("; "),
)))
}
}
/// Record a dropped best-effort broadcast: bump the run-long
/// [`lost_broadcasts`](Self::lost_broadcasts) counter, emit a
/// [`crate::monitor::EventKind::LostBroadcast`] on the shared timeline
/// if one is attached, and raise a `control_drop` alert on the record
/// stream. The caller has already logged the per-rank detail to stderr;
/// these are the structured, queryable twins of that log. `failures` is
/// the number of live ranks that did not receive the message.
///
/// Path is the root: a broadcast the coordinator could not deliver is a
/// cohort-level fault, and the per-rank breakdown is in the error text
/// the caller logged.
pub(super) fn note_lost_broadcast(&mut self, control: &str, failures: usize) {
self.lost_broadcasts += 1;
if let Some(ref tl) = self.timeline {
tl.event(crate::monitor::EventKind::LostBroadcast {
control: control.to_string(),
failures,
});
}
self.emit_alert(
crate::monitor::event_lane::EventClass::ControlDrop,
"root".to_string(),
format!("{control} did not reach {failures} live rank(s)"),
);
}
/// Send Shutdown to every rank. Called from [`Self::shutdown`];
/// kept public so callers running the coordinator inline can drop
/// it from a different point in their loop if needed.
pub fn shutdown_workers(&mut self) -> Result<()> {
self.broadcast_control(&ControlMsgWire::Shutdown)
}
/// Stop reader threads, send Shutdown to every connected rank,
/// join the threads, drop streams. Idempotent on the shutdown flag.
pub fn shutdown(mut self) -> Result<()> {
// Best-effort send Shutdown before tearing readers down. Ignore
// write errors here: a rank may already have exited.
let _ = self.shutdown_workers();
// Close the alert lane's open collapse windows so the tail of a
// flood lands in the persisted history, not only the live feed.
self.flush_alerts();
self.shutdown_flag.store(true, Ordering::SeqCst);
for handle_opt in self.reader_handles.iter_mut() {
if let Some(handle) = handle_opt.take() {
let _ = handle.join();
}
}
Ok(())
}
}
/// Short, payload-free label for a control message, used in the
/// [`EventKind::LostBroadcast`](crate::monitor::EventKind::LostBroadcast)
/// timeline trace. Exhaustive on purpose: a new `ControlMsgWire` variant
/// forces a label here rather than silently rendering as `"other"`.
/// Whether a failed best-effort broadcast deserves a `control_drop` alert.
///
/// A silently lost `SyncNow` / `DeclareDead` can leave the survivor cohort
/// waiting on a signal that never arrives, so mid-run drops are worth
/// shouting about. Two cases are not:
///
/// - `Shutdown` itself: a failed Shutdown send means the rank already exited,
/// which is the outcome Shutdown was asking for.
/// - **anything sent after shutdown was broadcast**: ranks are expected to be
/// exiting, and [`ClusterCoordinator::is_dead`] only knows heartbeat
/// staleness — a rank that finished *cleanly* is neither dead nor
/// reachable, so its closed socket would otherwise be reported as a lost
/// live-coordination signal.
///
/// The second case is why this exists. A 3-rank run that completed perfectly
/// raised `[critical] control_drop root — CoordHeartbeat did not reach 2 live
/// rank(s)` one log line after both of that host's ranks exited cleanly. A
/// clean run must not paint a red alert in the portal: an alert lane that
/// cries wolf trains the operator to ignore it, which costs more than the
/// missing trace ever would.
fn alerts_lost_broadcast(msg: &ControlMsgWire, phase: RunPhase) -> bool {
!matches!(msg, ControlMsgWire::Shutdown) && phase != RunPhase::ShutdownInitiated
}
fn control_label(msg: &ControlMsgWire) -> &'static str {
match msg {
ControlMsgWire::RequestParams => "RequestParams",
ControlMsgWire::Update { .. } => "Update",
ControlMsgWire::SyncNow => "SyncNow",
ControlMsgWire::StartEpoch(_) => "StartEpoch",
ControlMsgWire::ExtendPartition { .. } => "ExtendPartition",
ControlMsgWire::DeclareDead { .. } => "DeclareDead",
ControlMsgWire::RequestNewNcclId => "RequestNewNcclId",
ControlMsgWire::NewNcclSession { .. } => "NewNcclSession",
ControlMsgWire::Throttle => "Throttle",
ControlMsgWire::SetGlobalStep { .. } => "SetGlobalStep",
ControlMsgWire::Checkpoint { .. } => "Checkpoint",
ControlMsgWire::ExecuteEvalCallback { .. } => "ExecuteEvalCallback",
ControlMsgWire::SetEpochCallbackRole { .. } => "SetEpochCallbackRole",
ControlMsgWire::Shutdown => "Shutdown",
ControlMsgWire::ShutdownWithSave { .. } => "ShutdownWithSave",
ControlMsgWire::EpochAggregated(_) => "EpochAggregated",
ControlMsgWire::EvalBroadcast { .. } => "EvalBroadcast",
ControlMsgWire::StageAdvisory { .. } => "StageAdvisory",
ControlMsgWire::SaveConsensusModel { .. } => "SaveConsensusModel",
ControlMsgWire::CoordHeartbeat => "CoordHeartbeat",
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The rig case this predicate exists for: a 3-rank run that finished
/// cleanly still had its coordinator beacon at ranks that had already
/// exited, and the miss was classified `[critical] control_drop`.
/// A perfect run must not paint a red alert in the portal.
#[test]
fn a_heartbeat_missed_during_teardown_is_not_an_alert() {
assert!(!alerts_lost_broadcast(
&ControlMsgWire::CoordHeartbeat,
RunPhase::ShutdownInitiated
));
}
/// The same miss BEFORE shutdown is a genuine lost signal — the point of
/// the lane. Guards against fixing the false positive by muting the lane.
#[test]
fn the_same_heartbeat_missed_mid_run_still_alerts() {
assert!(alerts_lost_broadcast(
&ControlMsgWire::CoordHeartbeat,
RunPhase::Training
));
assert!(alerts_lost_broadcast(
&ControlMsgWire::CoordHeartbeat,
RunPhase::FinalEvalDispatched
));
}
/// A lost `SyncNow` can park the survivor cohort on a signal that never
/// arrives, so it must keep alerting in every pre-shutdown phase.
#[test]
fn losing_a_coordination_signal_mid_run_still_alerts() {
for phase in [RunPhase::Training, RunPhase::FinalEvalDispatched] {
assert!(alerts_lost_broadcast(&ControlMsgWire::SyncNow, phase));
assert!(alerts_lost_broadcast(
&ControlMsgWire::DeclareDead { rank: 1 },
phase
));
}
}
/// Shutdown's own failure was already exempt in every phase; a failed
/// Shutdown send means the rank did what Shutdown asked.
#[test]
fn a_failed_shutdown_send_is_never_an_alert() {
for phase in [
RunPhase::Training,
RunPhase::FinalEvalDispatched,
RunPhase::ShutdownInitiated,
] {
assert!(!alerts_lost_broadcast(&ControlMsgWire::Shutdown, phase));
}
}
}