use super::{StageSpec, WeightCache, recv_activation, resolve_device, send_activation};
use crate::Device;
use rlx_driver::{Node, ReduceKind};
pub use rlx_driver::{ProcessGroup, Topology};
use rlx_ir::DType;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
const TAG_CAPS: u32 = 42;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct FixedFunction {
pub stage_id: String,
pub input_elems: usize,
pub output_elems: usize,
pub dtype: DType,
}
impl FixedFunction {
pub fn new(
stage_id: impl Into<String>,
input_elems: usize,
output_elems: usize,
dtype: DType,
) -> Self {
Self {
stage_id: stage_id.into(),
input_elems,
output_elems,
dtype,
}
}
pub fn accepts(&self, spec: &StageSpec) -> Result<(), String> {
match spec.stage_id.as_deref() {
Some(id) if id == self.stage_id => Ok(()),
Some(id) => Err(format!(
"fixed-function rank implements stage [{}], cannot run [{id}]: \
an FPGA datapath is synthesized, not compiled",
self.stage_id
)),
None => Err(format!(
"fixed-function rank implements stage [{}], but the shipped \
StageSpec carries no stage_id — set StageSpec::stage_id to \
target pre-synthesized hardware",
self.stage_id
)),
}
}
}
pub trait FixedFunctionDevice: Send {
fn stage_id(&self) -> &str;
fn execute(&mut self, input: &[f32]) -> Result<Vec<f32>, String>;
}
pub struct LoopbackFixedFunction {
stage_id: String,
f: Box<dyn FnMut(&[f32]) -> Vec<f32> + Send>,
}
impl LoopbackFixedFunction {
pub fn new(
stage_id: impl Into<String>,
f: impl FnMut(&[f32]) -> Vec<f32> + Send + 'static,
) -> Self {
Self {
stage_id: stage_id.into(),
f: Box::new(f),
}
}
}
impl FixedFunctionDevice for LoopbackFixedFunction {
fn stage_id(&self) -> &str {
&self.stage_id
}
fn execute(&mut self, input: &[f32]) -> Result<Vec<f32>, String> {
Ok((self.f)(input))
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
pub enum NodeRole {
#[default]
Worker,
FixedFunction(FixedFunction),
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NodeCaps {
pub rank: u32,
pub role: NodeRole,
pub devices: Vec<String>,
pub platform: String,
}
impl NodeCaps {
pub fn accepts(&self, spec: &StageSpec) -> Result<(), String> {
match &self.role {
NodeRole::Worker => Ok(()),
NodeRole::FixedFunction(ff) => ff.accepts(spec),
}
}
pub fn can_train(&self) -> Result<(), String> {
match &self.role {
NodeRole::Worker => Ok(()),
NodeRole::FixedFunction(ff) => Err(format!(
"rank {} implements the pre-synthesized stage [{}] and cannot \
train: a bitstream has no backward pass, and a rank that never \
reduces stalls every rank that does",
self.rank, ff.stage_id
)),
}
}
}
pub const fn platform_tag() -> &'static str {
if cfg!(target_os = "ios") {
"ios"
} else if cfg!(target_os = "tvos") {
"tvos"
} else if cfg!(target_os = "watchos") {
"watchos"
} else if cfg!(target_os = "visionos") {
"visionos"
} else if cfg!(target_os = "android") {
"android"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_arch = "wasm32") {
"wasm"
} else {
"unknown"
}
}
#[derive(Clone, Debug)]
pub enum PeerSource {
Static(Vec<String>),
Discover {
disc_port: u16,
data_base: u16,
via: Option<String>,
},
}
impl Default for PeerSource {
fn default() -> Self {
Self::Static(Vec::new())
}
}
#[derive(Clone, Debug)]
pub struct NodeConfig {
pub rank: u32,
pub world: u32,
pub peers: PeerSource,
pub topology: Topology,
pub device: String,
pub heap_bytes: Option<usize>,
pub role: NodeRole,
}
impl NodeConfig {
pub fn new(rank: u32, world: u32) -> Self {
Self {
rank,
world,
peers: PeerSource::default(),
topology: Topology::Mesh,
device: "auto".into(),
heap_bytes: None,
role: NodeRole::Worker,
}
}
pub fn peers<I, S>(mut self, addrs: I) -> Result<Self, String>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let v: Vec<String> = addrs.into_iter().map(Into::into).collect();
if v.len() != self.world as usize && v.len() != 1 {
return Err(format!(
"peers: got {} address(es); expected {} (one per rank, for a \
mesh) or 1 (the coordinator, for a star)",
v.len(),
self.world
));
}
self.peers = PeerSource::Static(v);
Ok(self)
}
pub fn discover(mut self, disc_port: u16, data_base: u16) -> Self {
self.peers = PeerSource::Discover {
disc_port,
data_base,
via: None,
};
self
}
pub fn discover_via(mut self, host: impl Into<String>) -> Self {
if let PeerSource::Discover { via, .. } = &mut self.peers {
*via = Some(host.into());
} else {
self.peers = PeerSource::Discover {
disc_port: 29600,
data_base: 29500,
via: Some(host.into()),
};
}
self
}
pub fn topology(mut self, t: Topology) -> Self {
self.topology = t;
self
}
pub fn star(self) -> Self {
self.topology(Topology::Star)
}
pub fn mesh(self) -> Self {
self.topology(Topology::Mesh)
}
pub fn device(mut self, d: impl Into<String>) -> Self {
self.device = d.into();
self
}
pub fn heap_bytes(mut self, n: usize) -> Self {
self.heap_bytes = Some(n);
self
}
pub fn role(mut self, r: NodeRole) -> Self {
self.role = r;
self
}
pub fn fixed_function(self, ff: FixedFunction) -> Self {
self.role(NodeRole::FixedFunction(ff))
}
#[cfg(not(target_arch = "wasm32"))]
pub fn from_env() -> Result<Self, String> {
let var = |k: &str| std::env::var(k).ok();
let rank: u32 = var("RANK")
.as_deref()
.unwrap_or("0")
.parse()
.map_err(|_| "RANK must be an integer".to_string())?;
let world: u32 = var("WORLD")
.as_deref()
.unwrap_or("1")
.parse()
.map_err(|_| "WORLD must be an integer".to_string())?;
let mut cfg = NodeConfig::new(rank, world).device(var("DEVICE").unwrap_or("auto".into()));
let star =
var("TOPOLOGY").as_deref() == Some("star") || var("DIAL_OUT").is_some_and(|v| v != "0");
cfg = cfg.topology(if star { Topology::Star } else { Topology::Mesh });
if let Some(mb) = var("HEAP_MB").and_then(|v| v.parse::<usize>().ok()) {
cfg = cfg.heap_bytes(mb << 20);
}
if var("DISCOVER").is_some_and(|v| v != "0") {
let dp = var("DISC_PORT")
.and_then(|v| v.parse().ok())
.unwrap_or(29600);
let db = var("DATA_PORT")
.and_then(|v| v.parse().ok())
.unwrap_or(29500);
cfg = cfg.discover(dp, db);
if let Some(h) = var("DISCOVER_HOST") {
cfg = cfg.discover_via(h);
}
} else if let Some(peers) = var("PEERS") {
let addrs: Vec<String> = peers.split(',').map(|s| s.trim().to_string()).collect();
cfg = cfg.peers(addrs)?;
}
Ok(cfg)
}
pub fn connect(&self) -> std::io::Result<Arc<ProcessGroup>> {
let mut node = Node::new(self.rank, self.world).topology(self.topology);
if let Some(n) = self.heap_bytes {
node = node.heap_bytes(n);
}
node = match &self.peers {
PeerSource::Static(v) => node.peers(v.iter().map(String::as_str))?,
PeerSource::Discover {
disc_port,
data_base,
via,
} => {
let n = node.discover(*disc_port, *data_base);
match via {
Some(h) => n.discover_via(h.clone()),
None => n,
}
}
};
node.connect()
}
pub fn caps(&self) -> NodeCaps {
NodeCaps {
rank: self.rank,
role: self.role.clone(),
devices: crate::available_devices()
.into_iter()
.map(|d| d.as_arg().to_string())
.collect(),
platform: platform_tag().into(),
}
}
}
pub fn send_caps(group: &ProcessGroup, caps: &NodeCaps) -> Result<(), String> {
let bytes = serde_json::to_vec(caps).map_err(|e| e.to_string())?;
group
.transport()
.send_bytes(0, TAG_CAPS, &bytes)
.map_err(|e| format!("send_caps: {e}"))
}
pub fn collect_caps(group: &ProcessGroup) -> Result<Vec<NodeCaps>, String> {
let mut out = Vec::new();
for r in 1..group.world_size() {
let bytes = group
.transport()
.recv_bytes(r, TAG_CAPS)
.map_err(|e| format!("collect_caps(rank {r}): {e}"))?;
out.push(serde_json::from_slice(&bytes).map_err(|e| format!("NodeCaps(rank {r}): {e}"))?);
}
Ok(out)
}
#[derive(Clone)]
pub struct NodeControl {
steps: Option<usize>,
stop: Arc<AtomicBool>,
}
impl NodeControl {
pub fn unbounded() -> Self {
Self {
steps: None,
stop: Arc::new(AtomicBool::new(false)),
}
}
pub fn steps(n: usize) -> Self {
Self {
steps: Some(n),
stop: Arc::new(AtomicBool::new(false)),
}
}
pub fn stop_handle(&self) -> NodeStopHandle {
NodeStopHandle(self.stop.clone())
}
pub fn should_continue(&self, done: usize) -> bool {
if self.stop.load(Ordering::Relaxed) {
return false;
}
self.steps.is_none_or(|limit| done < limit)
}
}
impl Default for NodeControl {
fn default() -> Self {
Self::unbounded()
}
}
#[derive(Clone)]
pub struct NodeStopHandle(Arc<AtomicBool>);
impl NodeStopHandle {
pub fn stop(&self) {
self.0.store(true, Ordering::Relaxed);
}
pub fn is_stopped(&self) -> bool {
self.0.load(Ordering::Relaxed)
}
}
#[derive(Clone, Debug)]
pub struct NodeReport {
pub rank: u32,
pub device: Device,
pub platform: &'static str,
pub activations: usize,
}
pub fn serve_worker_n<F>(
group: &ProcessGroup,
caps: &NodeCaps,
resolve: F,
ctl: &NodeControl,
) -> Result<NodeReport, String>
where
F: FnMut(&str) -> Vec<f32>,
{
send_caps(group, caps)?;
let rank = group.rank();
let world = group.world_size();
let mut stage = super::recv_stage(group, resolve)?;
let next = if rank + 1 < world { rank + 1 } else { 0 };
let mut n = 0usize;
while ctl.should_continue(n) {
let Ok(x) = recv_activation(group, rank - 1) else {
break; };
let y = stage.run(&x);
send_activation(group, next, &y)?;
n += 1;
}
Ok(NodeReport {
rank,
device: stage.device,
platform: platform_tag(),
activations: n,
})
}
pub fn serve_worker<F>(group: &ProcessGroup, resolve: F) -> Result<NodeReport, String>
where
F: FnMut(&str) -> Vec<f32>,
{
let caps = NodeCaps {
rank: group.rank(),
role: NodeRole::Worker,
devices: crate::available_devices()
.into_iter()
.map(|d| d.as_arg().to_string())
.collect(),
platform: platform_tag().into(),
};
serve_worker_n(group, &caps, resolve, &NodeControl::unbounded())
}
pub fn serve_fixed_function_n(
group: &ProcessGroup,
ff: &FixedFunction,
board: &mut dyn FixedFunctionDevice,
ctl: &NodeControl,
) -> Result<NodeReport, String> {
if board.stage_id() != ff.stage_id {
return Err(format!(
"board implements [{}] but rank advertises [{}]",
board.stage_id(),
ff.stage_id
));
}
let caps = NodeCaps {
rank: group.rank(),
role: NodeRole::FixedFunction(ff.clone()),
devices: vec!["fpga".into()],
platform: platform_tag().into(),
};
send_caps(group, &caps)?;
let rank = group.rank();
let world = group.world_size();
let next = if rank + 1 < world { rank + 1 } else { 0 };
let mut n = 0usize;
while ctl.should_continue(n) {
let Ok(x) = recv_activation(group, rank - 1) else {
break;
};
if x.len() != ff.input_elems {
return Err(format!(
"fixed-function stage [{}] takes {} elems, got {}: the datapath \
is synthesized for one shape and cannot adapt",
ff.stage_id,
ff.input_elems,
x.len()
));
}
let y = board.execute(&x)?;
if y.len() != ff.output_elems {
return Err(format!(
"fixed-function stage [{}] declares {} output elems, board \
returned {}",
ff.stage_id,
ff.output_elems,
y.len()
));
}
send_activation(group, next, &y)?;
n += 1;
}
Ok(NodeReport {
rank,
device: Device::Cpu, platform: platform_tag(),
activations: n,
})
}
#[derive(Clone, Debug)]
pub struct TrainReport {
pub rank: u32,
pub platform: &'static str,
pub metrics: super::TrainMetrics,
pub params: Vec<(String, Vec<f32>)>,
}
pub fn serve_trainer<F>(
group: &ProcessGroup,
caps: &NodeCaps,
mut resolve: F,
log: bool,
) -> Result<TrainReport, String>
where
F: FnMut(&str) -> Vec<f32>,
{
send_caps(group, caps)?;
let mut spec = super::recv_train(group)?;
if spec.push_data {
super::pull_shards(group, &mut spec, &std::env::temp_dir())?;
}
let world = group.world_size();
let (metrics, params) = super::run_train(
&spec,
world,
|uri| resolve(uri),
|flat| mean_reduce(group, flat),
log,
)?;
Ok(TrainReport {
rank: group.rank(),
platform: platform_tag(),
metrics,
params,
})
}
pub fn mean_reduce(group: &ProcessGroup, flat: &[f32]) -> Vec<f32> {
let mut buf = flat.to_vec();
match group.all_reduce(&mut buf, ReduceKind::Mean) {
Ok(()) => buf,
Err(_) => vec![0.0; flat.len()],
}
}
pub fn serve_trainer_here<F>(
group: &ProcessGroup,
resolve: F,
log: bool,
) -> Result<TrainReport, String>
where
F: FnMut(&str) -> Vec<f32>,
{
let caps = NodeCaps {
rank: group.rank(),
role: NodeRole::Worker,
devices: crate::available_devices()
.into_iter()
.map(|d| d.as_arg().to_string())
.collect(),
platform: platform_tag().into(),
};
serve_trainer(group, &caps, resolve, log)
}
pub fn node_device(spec: &str) -> Device {
resolve_device(spec)
}
pub fn weight_cache() -> WeightCache {
WeightCache::new()
}