use std::io::{Read, Write};
use std::net::{SocketAddr, TcpStream};
use std::time::{Duration, Instant};
use crate::distributed::controller::{
self, DTYPE_F32, HANDSHAKE_MAGIC_CONTROLLER_ACK, HANDSHAKE_MAGIC_RANK, PROTOCOL_VERSION,
RoundFrame, RoundKind, TensorPayload,
};
use crate::distributed::wire::SessionSalt;
use crate::tensor::{DType, Device, Result, Tensor, TensorError};
const REDUCE_READ_DEADLINE_SECS: u64 = 120;
#[derive(Debug)]
pub struct CpuReduceClient {
stream: TcpStream,
rank_id: u32,
world_size: u32,
salt: SessionSalt,
prof_serialize_ns: u128,
prof_wire_ns: u128,
prof_deserialize_ns: u128,
prof_bytes: u64,
prof_count: u64,
prof_enabled: bool,
}
impl CpuReduceClient {
pub fn connect(
controller_addr: SocketAddr,
rank_id: u32,
world_size: u32,
salt: SessionSalt,
) -> Result<Self> {
if world_size == 0 {
return Err(TensorError::new(
"cpu_reduce: world_size must be > 0",
));
}
if rank_id >= world_size {
return Err(TensorError::new(&format!(
"cpu_reduce: rank_id {rank_id} must be < world_size {world_size}"
)));
}
let stream = crate::distributed::wire::connect_with_retry(
controller_addr,
"cpu_reduce",
)?;
let _ = stream.set_nodelay(true);
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.map_err(|e| TensorError::new(&format!("cpu_reduce: set_read_timeout: {e}")))?;
stream
.set_write_timeout(Some(crate::distributed::wire::write_stall_timeout()))
.map_err(|e| TensorError::new(&format!("cpu_reduce: set_write_timeout: {e}")))?;
let mut client = CpuReduceClient {
stream,
rank_id,
world_size,
salt,
prof_serialize_ns: 0,
prof_wire_ns: 0,
prof_deserialize_ns: 0,
prof_bytes: 0,
prof_count: 0,
prof_enabled: crate::log::enabled(crate::log::Verbosity::Debug),
};
client.send_handshake()?;
client.read_handshake_ack()?;
client
.stream
.set_read_timeout(Some(Duration::from_secs(
crate::distributed::wire::scaled_deadline_secs(REDUCE_READ_DEADLINE_SECS),
)))
.map_err(|e| {
TensorError::new(&format!("cpu_reduce: set reduce read deadline: {e}"))
})?;
Ok(client)
}
fn send_handshake(&mut self) -> Result<()> {
let mut buf = [0u8; 16];
buf[0..4].copy_from_slice(&HANDSHAKE_MAGIC_RANK.to_le_bytes());
buf[4..8].copy_from_slice(&PROTOCOL_VERSION.to_le_bytes());
buf[8..12].copy_from_slice(&self.rank_id.to_le_bytes());
buf[12..16].copy_from_slice(&self.world_size.to_le_bytes());
self.stream.write_all(&buf).map_err(|e| {
TensorError::new(&format!("cpu_reduce: handshake write failed: {e}"))
})?;
self.stream.flush().map_err(|e| {
TensorError::new(&format!("cpu_reduce: handshake flush failed: {e}"))
})?;
Ok(())
}
fn read_handshake_ack(&mut self) -> Result<()> {
let mut buf = [0u8; 8];
self.stream.read_exact(&mut buf).map_err(|e| {
TensorError::new(&format!(
"cpu_reduce: handshake ack read failed: {e} \
(controller may have rejected our handshake)"
))
})?;
let magic = u32::from_le_bytes(buf[0..4].try_into().unwrap());
if magic != HANDSHAKE_MAGIC_CONTROLLER_ACK {
return Err(TensorError::new(&format!(
"cpu_reduce: handshake ack magic 0x{magic:08x} != \
0x{HANDSHAKE_MAGIC_CONTROLLER_ACK:08x}"
)));
}
let proto_ver = u32::from_le_bytes(buf[4..8].try_into().unwrap());
if proto_ver != PROTOCOL_VERSION {
return Err(TensorError::new(&format!(
"cpu_reduce: controller protocol_version {proto_ver} != \
our version {PROTOCOL_VERSION}"
)));
}
Ok(())
}
pub fn world_size(&self) -> u32 {
self.world_size
}
pub fn all_reduce(&mut self, frame: &RoundFrame) -> Result<RoundFrame> {
write_framed_round(&mut self.stream, frame, &self.salt)?;
match read_framed_round(&mut self.stream, &self.salt)? {
Some(f) => Ok(f),
None => Err(TensorError::new(
"cpu_reduce: controller closed connection before sending averaged \
frame back (controller crashed, or another rank disconnected and \
triggered cluster-wide shutdown mid-round)",
)),
}
}
pub fn all_reduce_weighted(
&mut self,
tensors: &[&Tensor],
kind: RoundKind,
weight: f64,
) -> Result<(Vec<Tensor>, f64)> {
if !self.prof_enabled {
let mut frame = tensors_to_round_frame(tensors)?;
frame.kind = kind;
frame.weight = weight;
let reduced = self.all_reduce(&frame)?;
return Ok((round_frame_to_tensors(&reduced)?, reduced.weight));
}
let t0 = Instant::now();
let mut frame = tensors_to_round_frame(tensors)?;
frame.kind = kind;
frame.weight = weight;
let t1 = Instant::now();
let reduced = self.all_reduce(&frame)?;
let t2 = Instant::now();
let out = round_frame_to_tensors(&reduced)?;
let t3 = Instant::now();
self.prof_serialize_ns += (t1 - t0).as_nanos();
self.prof_wire_ns += (t2 - t1).as_nanos();
self.prof_deserialize_ns += (t3 - t2).as_nanos();
self.prof_bytes += frame
.tensors
.iter()
.map(|p| p.bytes.len() as u64)
.sum::<u64>();
self.prof_count += 1;
Ok((out, reduced.weight))
}
#[allow(dead_code)]
pub fn all_reduce_tensors(&mut self, tensors: &[&Tensor]) -> Result<Vec<Tensor>> {
Ok(self
.all_reduce_weighted(tensors, RoundKind::Model, 1.0)?
.0)
}
pub fn log_profile_summary(&self) {
if self.prof_count == 0 {
return;
}
let n = self.prof_count as f64;
let ser = self.prof_serialize_ns as f64 / 1e6;
let wire = self.prof_wire_ns as f64 / 1e6;
let de = self.prof_deserialize_ns as f64 / 1e6;
let total = (ser + wire + de).max(1e-9);
let mb = self.prof_bytes as f64 / 1e6;
let wire_s = self.prof_wire_ns as f64 / 1e9;
let mbps = if wire_s > 0.0 { (mb * 2.0) / wire_s } else { 0.0 };
eprintln!(
"[cpu-reduce-prof] rank={} reduces={} | serialize={:.0}ms ({:.0}%) \
wire={:.0}ms ({:.0}%) deserialize={:.0}ms ({:.0}%) | per-reduce \
ser={:.2}ms wire={:.2}ms de={:.2}ms bytes={:.2}MB | wire~{:.1}MB/s(up+down)",
self.rank_id,
self.prof_count,
ser,
100.0 * ser / total,
wire,
100.0 * wire / total,
de,
100.0 * de / total,
ser / n,
wire / n,
de / n,
mb / n,
mbps,
);
}
pub fn broadcast_from_root(
&mut self,
tensors: &[&Tensor],
root: u32,
) -> Result<Vec<Tensor>> {
if root >= self.world_size {
return Err(TensorError::new(&format!(
"cpu_reduce: broadcast root {root} >= world_size {}",
self.world_size,
)));
}
let contribution: Vec<Tensor> = if self.rank_id == root {
tensors
.iter()
.map(|t| {
let copy = Tensor::zeros_like(t)?;
copy.copy_(t, false)?;
Ok(copy)
})
.collect::<Result<Vec<_>>>()?
} else {
tensors
.iter()
.map(|t| Tensor::zeros_like(t))
.collect::<Result<Vec<_>>>()?
};
let refs: Vec<&Tensor> = contribution.iter().collect();
Ok(self
.all_reduce_weighted(&refs, RoundKind::Control, 0.0)?
.0)
}
pub fn all_reduce_per_rank_f64(&mut self, local: &mut [f64]) -> Result<()> {
let world_size = self.world_size as usize;
if local.len() != world_size {
return Err(TensorError::new(&format!(
"cpu_reduce: all_reduce_per_rank_f64: vector len ({}) must \
equal world_size ({})",
local.len(),
world_size,
)));
}
let vals: Vec<f32> = local.iter().map(|v| *v as f32).collect();
let tensor = Tensor::from_f32(
&vals,
&[world_size as i64],
Device::CPU,
)?;
let mut frame = tensors_to_round_frame(&[&tensor])?;
frame.kind = RoundKind::Control;
let averaged = self.all_reduce(&frame)?;
let out = round_frame_to_tensors(&averaged)?;
let avg = out
.first()
.ok_or_else(|| TensorError::new("cpu_reduce: count-gather returned empty frame"))?;
let out = avg.to_f32_vec()?;
for (dst, src) in local.iter_mut().zip(out) {
*dst = src as f64;
}
Ok(())
}
}
fn write_framed_round<W: Write>(
stream: &mut W,
frame: &RoundFrame,
salt: &SessionSalt,
) -> Result<()> {
let mut buf = Vec::new();
controller::write_round_frame(&mut buf, frame, salt)?;
crate::distributed::relay::mux::write_len_framed(stream, &buf)
}
fn read_framed_round<R: Read>(stream: &mut R, salt: &SessionSalt) -> Result<Option<RoundFrame>> {
match crate::distributed::relay::mux::read_len_framed(stream)? {
Some(buf) => controller::read_round_frame(&mut buf.as_slice(), salt),
None => Ok(None),
}
}
pub fn tensors_to_round_frame(tensors: &[&Tensor]) -> Result<RoundFrame> {
let mut payloads = Vec::with_capacity(tensors.len());
for (i, t) in tensors.iter().enumerate() {
if t.dtype() != DType::Float32 {
return Err(TensorError::new(&format!(
"cpu_reduce: tensor[{i}] dtype {:?} not supported in v1 \
(only Float32). Extend cpu_reduce.rs::tensors_to_round_frame \
and controller.rs::reduce_average together to add support.",
t.dtype()
)));
}
let shape_i64 = t.shape();
let shape: Vec<u32> = shape_i64
.iter()
.enumerate()
.map(|(d_idx, d)| {
u32::try_from(*d).map_err(|_| {
TensorError::new(&format!(
"cpu_reduce: tensor[{i}] dim[{d_idx}] = {d} doesn't fit in u32 \
(wire protocol uses u32 shape dims)"
))
})
})
.collect::<Result<_>>()?;
let bytes = t.to_blob()?;
payloads.push(TensorPayload {
dtype: DTYPE_F32,
shape,
bytes,
});
}
Ok(RoundFrame {
tensors: payloads,
kind: RoundKind::Model,
weight: 0.0,
})
}
pub fn round_frame_to_tensors(frame: &RoundFrame) -> Result<Vec<Tensor>> {
let mut out = Vec::with_capacity(frame.tensors.len());
for (i, p) in frame.tensors.iter().enumerate() {
if p.dtype != DTYPE_F32 {
return Err(TensorError::new(&format!(
"cpu_reduce: payload[{i}] dtype {} not supported in v1 \
(only DTYPE_F32 = 0)",
p.dtype
)));
}
if p.bytes.len() % 4 != 0 {
return Err(TensorError::new(&format!(
"cpu_reduce: payload[{i}] byte count {} not divisible by 4 \
(f32 element size)",
p.bytes.len()
)));
}
let n = p.bytes.len() / 4;
let mut data = Vec::with_capacity(n);
for j in 0..n {
let mut b = [0u8; 4];
b.copy_from_slice(&p.bytes[j * 4..(j + 1) * 4]);
data.push(f32::from_le_bytes(b));
}
let shape: Vec<i64> = p.shape.iter().map(|&d| d as i64).collect();
let numel_from_shape: i64 = shape.iter().product();
if numel_from_shape != n as i64 {
return Err(TensorError::new(&format!(
"cpu_reduce: payload[{i}] shape {shape:?} numel {numel_from_shape} \
!= bytes-derived numel {n}"
)));
}
out.push(Tensor::from_f32(&data, &shape, Device::CPU)?);
}
Ok(out)
}
#[cfg(test)]
#[path = "cpu_reduce_tests.rs"]
mod tests;