use std::collections::HashMap;
use std::io::{ErrorKind, Read, Write};
use std::net::TcpStream;
use hmac_sha256::HMAC;
use serde::{Deserialize, Serialize};
use crate::tensor::{Result, TensorError};
pub const CONTROL_HANDSHAKE_MAGIC_RANK: u32 = 0xF10D_17C2;
pub const CONTROL_HANDSHAKE_MAGIC_ACK: u32 = 0xF10D_17C3;
pub const CONTROL_FRAME_MAGIC: u32 = 0xF10D_17C4;
pub const CONTROL_PROTOCOL_VERSION: u32 = 2;
pub(crate) const CHANNEL_MAGIC_RENDEZVOUS: u32 = 0xF10D_17E0;
pub(crate) const CHANNEL_MAGIC_DATA: u32 = 0xF10D_17E1;
pub(crate) const CHANNEL_MAGIC_CONTROL: u32 = 0xF10D_17E2;
pub(crate) const CHANNEL_MAGIC_JOIN: u32 = 0xF10D_17E3;
pub(crate) const CHANNEL_MAGIC_HTTP_GET: u32 = u32::from_le_bytes(*b"GET ");
pub(crate) fn is_private_or_local(ip: std::net::IpAddr) -> bool {
match ip {
std::net::IpAddr::V4(v4) => {
v4.is_loopback()
|| v4.is_private()
|| v4.is_link_local()
|| (v4.octets()[0] == 100 && (v4.octets()[1] & 0xc0) == 64)
}
std::net::IpAddr::V6(v6) => {
if let Some(v4) = v6.to_ipv4_mapped() {
return is_private_or_local(std::net::IpAddr::V4(v4));
}
v6.is_loopback()
|| (v6.segments()[0] & 0xffc0) == 0xfe80
|| (v6.segments()[0] & 0xfe00) == 0xfc00
}
}
}
pub(crate) fn warn_cleartext_public_peer(what: &str, peer: std::net::SocketAddr) {
if is_private_or_local(peer.ip()) {
return;
}
static WARNED: std::sync::OnceLock<std::sync::Mutex<std::collections::HashSet<std::net::IpAddr>>> =
std::sync::OnceLock::new();
let warned = WARNED.get_or_init(|| std::sync::Mutex::new(std::collections::HashSet::new()));
if let Ok(mut set) = warned.lock() {
if !set.insert(peer.ip()) {
return;
}
}
eprintln!(
"flodl: WARNING: {what} peer {peer} is outside any private network \
range and this channel is CLEARTEXT (frames are HMAC-authenticated, \
not encrypted) — params and gradients cross this link readable. \
flodl's documented contract is a controlled/private network; for \
anything else route the traffic through an SSH tunnel \
(`tunnel: true` on the worker in cluster.yml) or an encrypted \
overlay (WireGuard / VPN)."
);
}
pub(crate) fn write_channel_magic<W: Write>(w: &mut W, magic: u32) -> Result<()> {
w.write_all(&magic.to_le_bytes()).map_err(|e| {
TensorError::new(&format!("wire: writing channel magic failed: {e}"))
})
}
pub(crate) fn expect_channel_magic<R: Read>(
r: &mut R,
expected: u32,
what: &str,
) -> Result<()> {
let mut buf = [0u8; 4];
r.read_exact(&mut buf).map_err(|e| {
TensorError::new(&format!("{what}: reading channel magic failed: {e}"))
})?;
let got = u32::from_le_bytes(buf);
if got != expected {
return Err(TensorError::new(&format!(
"{what}: channel magic 0x{got:08x} != 0x{expected:08x} \
(connection routed to the wrong channel?)"
)));
}
Ok(())
}
pub const MAX_CONTROL_PAYLOAD: usize = 16 * 1024 * 1024;
pub(crate) const READ_CHUNK: usize = 64 * 1024 * 1024;
pub(crate) const DEFAULT_FRAME_CEILING: usize = 1 << 30;
const FRAME_CEILING_FLOOR: usize = 64 * 1024 * 1024;
static FRAME_CEILING: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
pub(crate) fn frame_ceiling() -> usize {
*FRAME_CEILING.get().unwrap_or(&DEFAULT_FRAME_CEILING)
}
pub(crate) fn set_frame_ceiling(bytes: usize) {
if bytes == 0 {
return;
}
if FRAME_CEILING.set(bytes).is_ok() {
crate::verbose!(" wire: frame ceiling set to {bytes} bytes (model-derived)");
}
}
pub(crate) fn derive_frame_ceiling(model_wire_bytes: usize) -> usize {
model_wire_bytes
.saturating_mul(2)
.max(FRAME_CEILING_FLOOR)
}
pub(crate) fn tensors_wire_bytes(tensors: &[crate::tensor::Tensor]) -> usize {
tensors
.iter()
.map(|t| t.numel().max(0) as usize * t.dtype().element_size())
.sum()
}
pub(crate) const CONNECT_ATTEMPTS: u32 = 60;
pub(crate) const CONNECT_BACKOFF: std::time::Duration =
std::time::Duration::from_millis(500);
pub(crate) const WRITE_STALL_TIMEOUT: std::time::Duration =
std::time::Duration::from_secs(30);
pub(crate) const ENV_NET_TIMEOUT_SCALE: &str = "FLODL_NET_TIMEOUT_SCALE";
pub(crate) fn parse_net_timeout_scale(raw: Option<&str>) -> std::result::Result<f64, String> {
let Some(raw) = raw else { return Ok(1.0) };
let trimmed = raw.trim();
let parsed: f64 = trimmed.parse().map_err(|_| {
format!(
"{ENV_NET_TIMEOUT_SCALE}={trimmed:?} is not a number; expected a \
scale factor ≥ 0.1 (e.g. 3 for a slow WAN link, 0.5 for a \
fast-failure test rig)"
)
})?;
if !parsed.is_finite() || parsed < 0.1 {
return Err(format!(
"{ENV_NET_TIMEOUT_SCALE}={trimmed} is out of range; expected a \
finite scale factor ≥ 0.1 (0.1 keeps every deadline above the \
1s heartbeat cadence)"
));
}
Ok(parsed)
}
pub(crate) fn net_timeout_scale() -> f64 {
static SCALE: std::sync::OnceLock<f64> = std::sync::OnceLock::new();
*SCALE.get_or_init(|| {
let raw = std::env::var(ENV_NET_TIMEOUT_SCALE).ok();
match parse_net_timeout_scale(raw.as_deref()) {
Ok(s) => s,
Err(msg) => {
eprintln!("flodl: {msg}; using default scale 1.0");
1.0
}
}
})
}
pub(crate) fn connect_attempts() -> u32 {
((CONNECT_ATTEMPTS as f64 * net_timeout_scale()).ceil() as u32).max(1)
}
pub(crate) fn write_stall_timeout() -> std::time::Duration {
WRITE_STALL_TIMEOUT.mul_f64(net_timeout_scale())
}
pub(crate) fn scaled_deadline_secs(base_secs: u64) -> u64 {
((base_secs as f64 * net_timeout_scale()).ceil() as u64).max(1)
}
pub(crate) fn join_host_port(host: &str, port: u16) -> String {
if host.contains(':') && !host.starts_with('[') {
format!("[{host}]:{port}")
} else {
format!("{host}:{port}")
}
}
pub(crate) fn connect_with_retry<A>(
addr: A,
what: &str,
) -> Result<std::net::TcpStream>
where
A: std::net::ToSocketAddrs + std::fmt::Display + Copy,
{
let attempts = connect_attempts();
let mut last_err: Option<std::io::Error> = None;
for _ in 0..attempts {
match std::net::TcpStream::connect(addr) {
Ok(s) => return Ok(s),
Err(e) => {
last_err = Some(e);
std::thread::sleep(CONNECT_BACKOFF);
}
}
}
Err(TensorError::new(&format!(
"{what}: connect to {addr} failed after {attempts} attempts \
(~{}s): {}",
attempts as u64 * CONNECT_BACKOFF.as_millis() as u64 / 1000,
last_err
.map(|e| e.to_string())
.unwrap_or_else(|| "no error captured".into()),
)))
}
pub(crate) fn read_exact_incremental<R: Read>(
r: &mut R,
len: usize,
) -> std::io::Result<Vec<u8>> {
let mut buf: Vec<u8> = Vec::new();
while buf.len() < len {
let chunk = (len - buf.len()).min(READ_CHUNK);
let old_len = buf.len();
buf.resize(old_len + chunk, 0);
r.read_exact(&mut buf[old_len..])?;
}
Ok(buf)
}
pub const SESSION_SALT_BYTES: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum MsgKind {
Control = 0x01,
Timing = 0x02,
Metrics = 0x03,
ParamSnapshotMeta = 0x04,
Heartbeat = 0x05,
Rendezvous = 0x06,
Join = 0x07,
}
impl MsgKind {
pub fn from_u32(v: u32) -> Result<Self> {
match v {
0x01 => Ok(MsgKind::Control),
0x02 => Ok(MsgKind::Timing),
0x03 => Ok(MsgKind::Metrics),
0x04 => Ok(MsgKind::ParamSnapshotMeta),
0x05 => Ok(MsgKind::Heartbeat),
0x06 => Ok(MsgKind::Rendezvous),
0x07 => Ok(MsgKind::Join),
_ => Err(TensorError::new(&format!(
"wire: unknown MsgKind tag 0x{v:08x}"
))),
}
}
}
pub type SessionSalt = [u8; SESSION_SALT_BYTES];
pub fn hmac_sha256_64(salt: &SessionSalt, bytes: &[u8]) -> u64 {
let full: [u8; 32] = HMAC::mac(bytes, salt.as_slice());
u64::from_le_bytes(full[0..8].try_into().unwrap())
}
fn frame_mac(salt: &SessionSalt, kind: MsgKind, payload: &[u8]) -> u64 {
let mut macd = Vec::with_capacity(8 + payload.len());
macd.extend_from_slice(&(kind as u32).to_le_bytes());
macd.extend_from_slice(&(payload.len() as u32).to_le_bytes());
macd.extend_from_slice(payload);
hmac_sha256_64(salt, &macd)
}
const HS_RANK_BYTES: usize = 24;
const HS_ACK_BYTES: usize = 16;
fn hmac_first8(salt: &SessionSalt, bytes: &[u8]) -> [u8; 8] {
hmac_sha256_64(salt, bytes).to_le_bytes()
}
pub(crate) fn write_handshake_rank(
stream: &mut TcpStream,
rank_id: u32,
world_size: u32,
salt: &SessionSalt,
) -> Result<()> {
let mut buf = [0u8; HS_RANK_BYTES];
buf[0..4].copy_from_slice(&CONTROL_HANDSHAKE_MAGIC_RANK.to_le_bytes());
buf[4..8].copy_from_slice(&CONTROL_PROTOCOL_VERSION.to_le_bytes());
buf[8..12].copy_from_slice(&rank_id.to_le_bytes());
buf[12..16].copy_from_slice(&world_size.to_le_bytes());
let tag = hmac_first8(salt, &buf[0..16]);
buf[16..24].copy_from_slice(&tag);
stream.write_all(&buf).map_err(|e| {
TensorError::new(&format!("wire: handshake write: {e}"))
})
}
pub(crate) fn read_handshake_rank(
stream: &mut TcpStream,
expected_world_size: u32,
salt: &SessionSalt,
) -> Result<u32> {
let mut buf = [0u8; HS_RANK_BYTES];
stream.read_exact(&mut buf).map_err(|e| {
TensorError::new(&format!("wire: handshake read: {e}"))
})?;
let magic = u32::from_le_bytes(buf[0..4].try_into().unwrap());
if magic != CONTROL_HANDSHAKE_MAGIC_RANK {
return Err(TensorError::new(&format!(
"wire: handshake magic 0x{magic:08x} != 0x{CONTROL_HANDSHAKE_MAGIC_RANK:08x}"
)));
}
let version = u32::from_le_bytes(buf[4..8].try_into().unwrap());
if version != CONTROL_PROTOCOL_VERSION {
return Err(TensorError::new(&format!(
"wire: handshake version {version} != {CONTROL_PROTOCOL_VERSION}"
)));
}
let rank_id = u32::from_le_bytes(buf[8..12].try_into().unwrap());
let world_size = u32::from_le_bytes(buf[12..16].try_into().unwrap());
if world_size != expected_world_size {
return Err(TensorError::new(&format!(
"wire: handshake world_size {world_size} != expected {expected_world_size}"
)));
}
let expected_tag = hmac_first8(salt, &buf[0..16]);
let got_tag: [u8; 8] = buf[16..24].try_into().unwrap();
if expected_tag != got_tag {
return Err(TensorError::new(
"wire: handshake HMAC verification failed; \
session salt disagreement (rank from a different training session, \
or wrong key configured)",
));
}
Ok(rank_id)
}
pub(crate) fn write_handshake_ack(stream: &mut TcpStream, salt: &SessionSalt) -> Result<()> {
let mut buf = [0u8; HS_ACK_BYTES];
buf[0..4].copy_from_slice(&CONTROL_HANDSHAKE_MAGIC_ACK.to_le_bytes());
buf[4..8].copy_from_slice(&CONTROL_PROTOCOL_VERSION.to_le_bytes());
let tag = hmac_first8(salt, &buf[0..8]);
buf[8..16].copy_from_slice(&tag);
stream.write_all(&buf).map_err(|e| {
TensorError::new(&format!("wire: handshake ack write: {e}"))
})
}
pub(crate) fn read_handshake_ack(stream: &mut TcpStream, salt: &SessionSalt) -> Result<()> {
let mut buf = [0u8; HS_ACK_BYTES];
stream.read_exact(&mut buf).map_err(|e| {
TensorError::new(&format!(
"wire: handshake ack read failed: {e} \
(coordinator may have rejected our handshake)"
))
})?;
let magic = u32::from_le_bytes(buf[0..4].try_into().unwrap());
if magic != CONTROL_HANDSHAKE_MAGIC_ACK {
return Err(TensorError::new(&format!(
"wire: handshake ack magic 0x{magic:08x} != 0x{CONTROL_HANDSHAKE_MAGIC_ACK:08x}"
)));
}
let version = u32::from_le_bytes(buf[4..8].try_into().unwrap());
if version != CONTROL_PROTOCOL_VERSION {
return Err(TensorError::new(&format!(
"wire: handshake ack version {version} != {CONTROL_PROTOCOL_VERSION}"
)));
}
let expected_tag = hmac_first8(salt, &buf[0..8]);
let got: [u8; 8] = buf[8..16].try_into().unwrap();
if expected_tag != got {
return Err(TensorError::new(
"wire: handshake ack HMAC verification failed; \
session salt disagreement (worker holds a different salt than coordinator)",
));
}
Ok(())
}
#[cfg(feature = "rng")]
pub fn generate_session_salt() -> SessionSalt {
use rand::Rng;
let mut buf = [0u8; SESSION_SALT_BYTES];
rand::rng().fill_bytes(&mut buf);
buf
}
pub fn salt_to_hex(salt: &SessionSalt) -> String {
crate::distributed::cluster::hex_encode(salt)
}
pub fn salt_from_hex(s: &str) -> Result<SessionSalt> {
let trimmed = s.trim();
if trimmed.len() != SESSION_SALT_BYTES * 2 {
return Err(TensorError::new(&format!(
"wire: session salt hex must be {} chars (got {})",
SESSION_SALT_BYTES * 2,
trimmed.len()
)));
}
let bytes = crate::distributed::cluster::hex_decode(trimmed)
.map_err(|e| TensorError::new(&format!("wire: session salt hex-decode: {e}")))?;
let mut out = [0u8; SESSION_SALT_BYTES];
out.copy_from_slice(&bytes);
Ok(out)
}
fn bincode_config() -> impl bincode::config::Config {
bincode::config::standard()
}
fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>> {
bincode::serde::encode_to_vec(value, bincode_config())
.map_err(|e| TensorError::new(&format!("wire: bincode encode failed: {e}")))
}
fn decode<T: for<'de> Deserialize<'de>>(bytes: &[u8]) -> Result<T> {
let (v, _used) = bincode::serde::decode_from_slice(bytes, bincode_config())
.map_err(|e| TensorError::new(&format!("wire: bincode decode failed: {e}")))?;
Ok(v)
}
#[derive(Debug, Clone, PartialEq)]
pub struct ControlFrame {
pub kind: MsgKind,
pub auth_tag: u64,
pub payload: Vec<u8>,
}
impl ControlFrame {
pub fn encode<T: Serialize>(
salt: &SessionSalt,
kind: MsgKind,
msg: &T,
) -> Result<Self> {
let payload = encode(msg)?;
let auth_tag = frame_mac(salt, kind, &payload);
Ok(ControlFrame {
kind,
auth_tag,
payload,
})
}
pub fn decode<T: for<'de> Deserialize<'de>>(&self) -> Result<T> {
decode(&self.payload)
}
pub fn write_to<W: Write>(&self, w: &mut W) -> Result<()> {
let mut hdr = [0u8; 24];
hdr[0..4].copy_from_slice(&CONTROL_FRAME_MAGIC.to_le_bytes());
hdr[4..8].copy_from_slice(&CONTROL_PROTOCOL_VERSION.to_le_bytes());
hdr[8..16].copy_from_slice(&self.auth_tag.to_le_bytes());
hdr[16..20].copy_from_slice(&(self.kind as u32).to_le_bytes());
let payload_len = u32::try_from(self.payload.len()).map_err(|_| {
TensorError::new(&format!(
"wire: payload too large: {} bytes (max {} bytes)",
self.payload.len(),
u32::MAX
))
})?;
hdr[20..24].copy_from_slice(&payload_len.to_le_bytes());
w.write_all(&hdr).map_err(|e| {
TensorError::new(&format!("wire: ControlFrame header write failed: {e}"))
})?;
w.write_all(&self.payload).map_err(|e| {
TensorError::new(&format!("wire: ControlFrame payload write failed: {e}"))
})?;
Ok(())
}
pub fn read_from<R: Read>(r: &mut R, salt: &SessionSalt) -> Result<Option<Self>> {
let mut hdr = [0u8; 24];
match r.read_exact(&mut hdr) {
Ok(()) => {}
Err(e)
if matches!(
e.kind(),
ErrorKind::UnexpectedEof | ErrorKind::ConnectionReset
) =>
{
return Ok(None);
}
Err(e) => {
return Err(TensorError::new(&format!(
"wire: ControlFrame header read failed: {e}"
)));
}
}
Self::finish_read_from(hdr, r, salt).map(Some)
}
fn finish_read_from<R: Read>(
hdr: [u8; 24],
r: &mut R,
salt: &SessionSalt,
) -> Result<Self> {
let magic = u32::from_le_bytes(hdr[0..4].try_into().unwrap());
if magic != CONTROL_FRAME_MAGIC {
return Err(TensorError::new(&format!(
"wire: ControlFrame magic 0x{magic:08x} != 0x{CONTROL_FRAME_MAGIC:08x}"
)));
}
let version = u32::from_le_bytes(hdr[4..8].try_into().unwrap());
if version != CONTROL_PROTOCOL_VERSION {
return Err(TensorError::new(&format!(
"wire: ControlFrame version {version} != {CONTROL_PROTOCOL_VERSION}"
)));
}
let auth_tag = u64::from_le_bytes(hdr[8..16].try_into().unwrap());
let kind_u32 = u32::from_le_bytes(hdr[16..20].try_into().unwrap());
let kind = MsgKind::from_u32(kind_u32)?;
let payload_len = u32::from_le_bytes(hdr[20..24].try_into().unwrap()) as usize;
if payload_len > MAX_CONTROL_PAYLOAD {
return Err(TensorError::new(&format!(
"wire: ControlFrame payload_len {payload_len} exceeds \
MAX_CONTROL_PAYLOAD {MAX_CONTROL_PAYLOAD} (kind={kind:?}); \
rejecting before allocation"
)));
}
let mut payload = vec![0u8; payload_len];
r.read_exact(&mut payload).map_err(|e| {
TensorError::new(&format!(
"wire: ControlFrame payload read failed (kind={kind:?}, len={payload_len}): {e}"
))
})?;
let actual = frame_mac(salt, kind, &payload);
if actual != auth_tag {
return Err(TensorError::new(&format!(
"wire: ControlFrame HMAC verification failed (computed \
0x{actual:016x}, header carried 0x{auth_tag:016x}); session \
salt disagreement, tampered frame, or payload corruption \
(kind={kind:?}, len={payload_len})"
)));
}
Ok(ControlFrame {
kind,
auth_tag,
payload,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EpochPlanWire {
pub epoch: u64,
pub partition_offset: u64,
pub partition_size: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ControlMsgWire {
RequestParams,
Update {
version: u64,
next_plan: Option<EpochPlanWire>,
},
SyncNow,
StartEpoch(EpochPlanWire),
ExtendPartition {
partition_offset: u64,
partition_size: u64,
},
DeclareDead { rank: u64 },
RequestNewNcclId,
NewNcclSession {
uid_bytes: Vec<u8>,
new_rank: u64,
new_world_size: u64,
},
Throttle,
SetGlobalStep { global_step: u64 },
Checkpoint { version: u64, target_rank: u64 },
ExecuteEvalCallback {
schedule_id: u64,
epoch: u64,
target_rank: u64,
},
SetEpochCallbackRole { rank: u64 },
Shutdown,
ShutdownWithSave { reason: u8 },
EpochAggregated(Box<EpochMetricsWire>),
EvalBroadcast { epoch: u64, metric: f64 },
SaveConsensusModel { target_rank: u64 },
CoordHeartbeat,
StageAdvisory {
counts: Vec<u64>,
segments: Vec<(u64, Vec<(u64, u64)>)>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IntentKind {
EvalNow,
CheckpointNow,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum TimingMsgWire {
Batch {
rank: u64,
batch_ms: f64,
#[serde(default)]
data_ms: f64,
step_count: u64,
param_norm: Option<f64>,
batch_loss: f64,
sync_divergence: Option<f64>,
},
SyncAck {
rank: u64,
step_count: u64,
divergence: Option<f64>,
post_norm: Option<f64>,
pre_norm: Option<f64>,
},
Exiting {
rank: u64,
},
LrUpdate {
rank: u64,
lr: f64,
},
Intent {
rank: u64,
kind: IntentKind,
},
Heartbeat {
rank: u64,
step_count: u64,
},
SnapshotReady { rank: u64 },
EvalResult {
rank: u64,
schedule_id: u64,
epoch: u64,
metric: f64,
elapsed_ms: f64,
error: Option<String>,
},
CheckpointResult {
rank: u64,
version: u64,
elapsed_ms: f64,
error: Option<String>,
},
NewNcclIdGenerated {
rank: u64,
uid_bytes: Vec<u8>,
},
EpochFnElapsed {
rank: u64,
epoch: u64,
elapsed_ms: f64,
},
DashboardRegister {
rank: u64,
port: u16,
},
DashboardSetSvg {
rank: u64,
svg: String,
label: Option<String>,
hash: Option<String>,
},
DashboardSetMetadata {
rank: u64,
json: String,
},
DashboardSetHardware {
rank: u64,
summary: String,
},
ResourceSample {
rank: u64,
sample: ResourceSampleWire,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct GpuSnapshotWire {
pub device_index: u8,
pub name: String,
pub util_percent: Option<f32>,
pub vram_allocated_bytes: Option<u64>,
pub vram_total_bytes: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct ResourceSampleWire {
pub cpu_percent: Option<f32>,
pub ram_used_bytes: Option<u64>,
pub ram_total_bytes: Option<u64>,
pub gpu_util_percent: Option<f32>,
pub vram_total_bytes: Option<u64>,
pub vram_allocated_bytes: Option<u64>,
pub aggregate_rank: Option<u8>,
pub gpus: Vec<GpuSnapshotWire>,
}
impl From<crate::monitor::GpuSnapshot> for GpuSnapshotWire {
fn from(g: crate::monitor::GpuSnapshot) -> Self {
GpuSnapshotWire {
device_index: g.device_index,
name: g.name,
util_percent: g.util_percent,
vram_allocated_bytes: g.vram_allocated_bytes,
vram_total_bytes: g.vram_total_bytes,
}
}
}
impl From<GpuSnapshotWire> for crate::monitor::GpuSnapshot {
fn from(w: GpuSnapshotWire) -> Self {
crate::monitor::GpuSnapshot {
device_index: w.device_index,
name: w.name,
util_percent: w.util_percent,
vram_allocated_bytes: w.vram_allocated_bytes,
vram_total_bytes: w.vram_total_bytes,
}
}
}
impl From<crate::monitor::ResourceSample> for ResourceSampleWire {
fn from(s: crate::monitor::ResourceSample) -> Self {
ResourceSampleWire {
cpu_percent: s.cpu_percent,
ram_used_bytes: s.ram_used_bytes,
ram_total_bytes: s.ram_total_bytes,
gpu_util_percent: s.gpu_util_percent,
vram_total_bytes: s.vram_total_bytes,
vram_allocated_bytes: s.vram_allocated_bytes,
aggregate_rank: s.aggregate_rank,
gpus: s.gpus.into_iter().map(Into::into).collect(),
}
}
}
impl From<ResourceSampleWire> for crate::monitor::ResourceSample {
fn from(w: ResourceSampleWire) -> Self {
crate::monitor::ResourceSample {
cpu_percent: w.cpu_percent,
ram_used_bytes: w.ram_used_bytes,
ram_total_bytes: w.ram_total_bytes,
gpu_util_percent: w.gpu_util_percent,
vram_total_bytes: w.vram_total_bytes,
vram_allocated_bytes: w.vram_allocated_bytes,
aggregate_rank: w.aggregate_rank,
gpus: w.gpus.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct MetricsMsgWire {
pub rank: u64,
pub epoch: u64,
pub avg_loss: f64,
pub batches_processed: u64,
pub epoch_ms: f64,
pub samples_processed: u64,
pub share_complete_ms: f64,
pub compute_only_ms: f64,
pub data_starve_ms: f64,
pub scalars: HashMap<String, (f64, u64)>,
#[serde(default)]
pub resources: Option<ResourceSampleWire>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct EpochMetricsWire {
pub epoch: u64,
pub scalars: HashMap<String, f64>,
pub per_rank: Vec<HashMap<String, f64>>,
pub avg_loss: f64,
pub epoch_ms: f64,
pub per_rank_throughput: Vec<f64>,
pub per_rank_batch_share: Vec<f64>,
pub per_rank_share_complete_ms: Vec<f64>,
pub per_rank_compute_only_ms: Vec<f64>,
pub per_rank_data_starve_ms: Vec<f64>,
pub device_indices: Vec<u8>,
pub per_rank_loss: Vec<Option<f64>>,
pub per_rank_samples: Vec<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RendezvousRole {
Generate,
Wait,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RendezvousMsgWire {
Hello {
dataset_sig: [u8; 32],
global_rank: u32,
host_name: String,
},
Role(RendezvousRole),
Uid {
uid_bytes: Vec<u8>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum JoinMsgWire {
Hello {
host: String,
local_devices: Vec<u8>,
gpus: Vec<String>,
libtorch: String,
dataset_sig: [u8; 32],
},
Accept {
ranks: Vec<u32>,
salt_hex: Option<String>,
formation_wait_secs: u64,
},
Reject {
reason: String,
},
WorldFormed {
envelope_hex: String,
relay_spec_hex: Option<String>,
},
RankExited {
rank: u32,
code: i32,
},
Abort {
reason: String,
},
}
#[cfg(test)]
#[path = "wire_tests.rs"]
mod tests;