1use crate::net::{DEFAULT_HEAP_BYTES, NetTransport, TcpTransport};
41use crate::transport::ProcessGroup;
42use std::collections::BTreeMap;
43use std::io;
44use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, ToSocketAddrs, UdpSocket};
45use std::sync::Arc;
46use std::sync::atomic::{AtomicBool, Ordering};
47use std::time::Duration;
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum Topology {
52 Mesh,
55 Star,
59}
60
61#[derive(Clone, Debug)]
62enum PeerSpec {
63 Static(Vec<SocketAddr>),
64 Discover { disc_port: u16, data_base: u16 },
65}
66
67pub struct Node {
69 rank: u32,
70 world: u32,
71 peers: PeerSpec,
72 topology: Topology,
73 heap_bytes: usize,
74 discover_host: Option<String>,
78}
79
80impl Node {
81 pub fn new(rank: u32, world: u32) -> Self {
84 Self {
85 rank,
86 world,
87 peers: PeerSpec::Static(Vec::new()),
88 topology: Topology::Mesh,
89 heap_bytes: DEFAULT_HEAP_BYTES,
90 discover_host: None,
91 }
92 }
93
94 pub fn discover_via(mut self, host: impl Into<String>) -> Self {
99 self.discover_host = Some(host.into());
100 self
101 }
102
103 pub fn peers<A: ToSocketAddrs>(
106 mut self,
107 addrs: impl IntoIterator<Item = A>,
108 ) -> io::Result<Self> {
109 let mut v = Vec::new();
110 for a in addrs {
111 let sa = a.to_socket_addrs()?.next().ok_or_else(|| {
112 io::Error::new(io::ErrorKind::InvalidInput, "peer resolved to no address")
113 })?;
114 v.push(sa);
115 }
116 self.peers = PeerSpec::Static(v);
117 Ok(self)
118 }
119
120 pub fn discover(mut self, disc_port: u16, data_base: u16) -> Self {
124 self.peers = PeerSpec::Discover {
125 disc_port,
126 data_base,
127 };
128 self
129 }
130
131 pub fn topology(mut self, t: Topology) -> Self {
132 self.topology = t;
133 self
134 }
135
136 pub fn heap_bytes(mut self, n: usize) -> Self {
138 self.heap_bytes = n;
139 self
140 }
141
142 pub fn rank(&self) -> u32 {
143 self.rank
144 }
145 pub fn world(&self) -> u32 {
146 self.world
147 }
148
149 pub fn from_env() -> Result<Self, String> {
156 let var = |k: &str| std::env::var(k).ok();
157 let rank: u32 = var("RANK")
158 .as_deref()
159 .unwrap_or("0")
160 .parse()
161 .map_err(|_| "RANK must be an integer".to_string())?;
162 let world: u32 = var("WORLD")
163 .as_deref()
164 .unwrap_or("1")
165 .parse()
166 .map_err(|_| "WORLD must be an integer".to_string())?;
167 let mut node = Node::new(rank, world);
168
169 let star =
170 var("TOPOLOGY").as_deref() == Some("star") || var("DIAL_OUT").is_some_and(|v| v != "0");
171 node = node.topology(if star { Topology::Star } else { Topology::Mesh });
172
173 if let Some(mb) = var("HEAP_MB").and_then(|v| v.parse::<usize>().ok()) {
174 node = node.heap_bytes(mb << 20);
175 }
176
177 if var("DISCOVER").is_some_and(|v| v != "0") {
178 let dp = var("DISC_PORT")
179 .and_then(|v| v.parse().ok())
180 .unwrap_or(29600);
181 let db = var("DATA_PORT")
182 .and_then(|v| v.parse().ok())
183 .unwrap_or(29500);
184 node = node.discover(dp, db);
185 if let Some(h) = var("DISCOVER_HOST") {
186 node = node.discover_via(h);
187 }
188 } else {
189 let peers = var("PEERS").unwrap_or_else(|| "127.0.0.1:29500,127.0.0.1:29501".into());
190 let addrs: Vec<String> = peers.split(',').map(|s| s.trim().to_string()).collect();
191 node = node
192 .peers(addrs.iter().map(String::as_str))
193 .map_err(|e| format!("PEERS: {e}"))?;
194 }
195 Ok(node)
196 }
197
198 pub fn connect(self) -> io::Result<Arc<ProcessGroup>> {
200 if self.topology == Topology::Star
206 && let PeerSpec::Discover {
207 disc_port,
208 data_base,
209 } = &self.peers
210 {
211 let (disc_port, data_base) = (*disc_port, *data_base);
212 let transport = if self.rank == 0 {
213 let listener = TcpListener::bind(("0.0.0.0", data_base))?;
214 let stop = Arc::new(AtomicBool::new(false));
215 let ann = stop.clone();
216 std::thread::spawn(move || announce_coordinator(data_base, disc_port, &ann));
217 #[cfg(feature = "mdns")]
218 let _mdns = mdns_advertise(data_base); let t = NetTransport::coordinator_listen(self.world, listener, self.heap_bytes);
220 stop.store(true, Ordering::SeqCst);
221 t?
222 } else {
223 let coord = discover_coordinator(disc_port, self.discover_host.as_deref())?;
224 NetTransport::worker_dial(self.rank, self.world, coord, self.heap_bytes)?
225 };
226 return Ok(Arc::new(ProcessGroup::new(Arc::new(transport))));
227 }
228
229 let peers = match self.peers {
231 PeerSpec::Static(v) => v,
232 PeerSpec::Discover {
233 disc_port,
234 data_base,
235 } => discover_peers(self.rank, self.world, disc_port, data_base),
236 };
237 let transport = match self.topology {
238 Topology::Mesh => {
239 if peers.len() != self.world as usize {
240 return Err(io::Error::new(
241 io::ErrorKind::InvalidInput,
242 format!(
243 "mesh needs WORLD={} peer addresses, got {}",
244 self.world,
245 peers.len()
246 ),
247 ));
248 }
249 TcpTransport::bind(self.rank, self.world, peers, self.heap_bytes)?
250 }
251 Topology::Star => {
252 let coord = *peers.first().ok_or_else(|| {
253 io::Error::new(
254 io::ErrorKind::InvalidInput,
255 "star needs the coordinator address (peers[0])",
256 )
257 })?;
258 if self.rank == 0 {
259 let listener = TcpListener::bind(coord)?;
260 NetTransport::coordinator_listen(self.world, listener, self.heap_bytes)?
261 } else {
262 NetTransport::worker_dial(self.rank, self.world, coord, self.heap_bytes)?
263 }
264 }
265 };
266 Ok(Arc::new(ProcessGroup::new(Arc::new(transport))))
267 }
268}
269
270pub fn local_ip() -> IpAddr {
273 UdpSocket::bind("0.0.0.0:0")
274 .and_then(|s| {
275 s.connect("8.8.8.8:80")?; s.local_addr()
277 })
278 .map(|a| a.ip())
279 .unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST))
280}
281
282pub fn discover_peers(rank: u32, world: u32, disc_port: u16, data_base: u16) -> Vec<SocketAddr> {
286 let my_addr = SocketAddr::new(local_ip(), data_base + rank as u16);
287 let sock = UdpSocket::bind(("0.0.0.0", disc_port)).expect("discovery bind");
288 sock.set_broadcast(true).ok();
289 sock.set_read_timeout(Some(Duration::from_millis(150))).ok();
290 let bcast = SocketAddr::new(IpAddr::V4(Ipv4Addr::BROADCAST), disc_port);
291 let msg = format!("RLXDISC {rank} {my_addr}");
292
293 let mut peers: BTreeMap<u32, SocketAddr> = BTreeMap::new();
294 peers.insert(rank, my_addr);
295 let mut buf = [0u8; 256];
296 while (peers.len() as u32) < world {
297 let _ = sock.send_to(msg.as_bytes(), bcast);
298 if let Ok((n, _)) = sock.recv_from(&mut buf)
299 && let Ok(s) = std::str::from_utf8(&buf[..n])
300 {
301 let mut it = s.split_whitespace();
302 if it.next() == Some("RLXDISC")
303 && let (Some(r), Some(a)) = (it.next(), it.next())
304 && let (Ok(r), Ok(a)) = (r.parse::<u32>(), a.parse::<SocketAddr>())
305 {
306 peers.insert(r, a);
307 }
308 }
309 }
310 for _ in 0..5 {
312 let _ = sock.send_to(msg.as_bytes(), bcast);
313 }
314 peers.into_values().collect()
315}
316
317fn parse_rlxport(b: &[u8]) -> Option<u16> {
318 let s = std::str::from_utf8(b).ok()?;
319 let mut it = s.split_whitespace();
320 if it.next() != Some("RLXPORT") {
321 return None;
322 }
323 it.next()?.parse().ok()
324}
325
326pub fn announce_coordinator(data_port: u16, disc_port: u16, stop: &AtomicBool) {
335 let Ok(sock) = UdpSocket::bind(("0.0.0.0", disc_port)) else {
336 return;
337 };
338 sock.set_read_timeout(Some(Duration::from_millis(200))).ok();
339 let msg = format!("RLXPORT {data_port}");
340 let mut buf = [0u8; 64];
341 while !stop.load(Ordering::SeqCst) {
342 if let Ok((n, from)) = sock.recv_from(&mut buf)
343 && buf[..n].starts_with(b"RLXQ")
344 {
345 let _ = sock.send_to(msg.as_bytes(), from); }
347 }
348}
349
350pub fn discover_coordinator(disc_port: u16, host: Option<&str>) -> io::Result<SocketAddr> {
355 let mut buf = [0u8; 64];
356 let no_addr = || {
357 io::Error::new(
358 io::ErrorKind::InvalidInput,
359 "discover host resolved to no address",
360 )
361 };
362 match host {
363 Some(h) => {
364 let target = (h, disc_port)
365 .to_socket_addrs()?
366 .next()
367 .ok_or_else(no_addr)?;
368 let sock = UdpSocket::bind(("0.0.0.0", 0))?;
369 sock.set_read_timeout(Some(Duration::from_millis(300))).ok();
370 loop {
371 sock.send_to(b"RLXQ", target)?;
372 if let Ok((n, _)) = sock.recv_from(&mut buf)
373 && let Some(port) = parse_rlxport(&buf[..n])
374 {
375 return (h, port).to_socket_addrs()?.next().ok_or_else(no_addr);
376 }
377 }
378 }
379 None => {
380 let _ = disc_port;
381 #[cfg(feature = "mdns")]
382 {
383 mdns_discover(Duration::from_secs(15)).ok_or_else(|| {
384 io::Error::new(
385 io::ErrorKind::NotFound,
386 "mDNS: no _rlx-coord._udp coordinator found on the LAN",
387 )
388 })
389 }
390 #[cfg(not(feature = "mdns"))]
391 Err(io::Error::new(
392 io::ErrorKind::InvalidInput,
393 "no coordinator address — set DISCOVER_HOST (LAN IP / host.docker.internal / \
394 tailnet name) for unicast discovery, or build with the `mdns` feature for \
395 zero-config LAN discovery",
396 ))
397 }
398 }
399}
400
401#[cfg(feature = "mdns")]
406pub fn mdns_advertise(data_port: u16) -> Option<mdns_sd::ServiceDaemon> {
407 use mdns_sd::{ServiceDaemon, ServiceInfo};
408 let mdns = ServiceDaemon::new().ok()?;
409 let ip = local_ip().to_string();
410 let props: &[(&str, &str)] = &[];
411 let info = ServiceInfo::new(
412 "_rlx-coord._udp.local.",
413 "coord",
414 "rlx-coord.local.",
415 ip.as_str(),
416 data_port,
417 props,
418 )
419 .ok()?;
420 mdns.register(info).ok()?;
421 Some(mdns)
422}
423
424#[cfg(feature = "mdns")]
426pub fn mdns_discover(timeout: Duration) -> Option<SocketAddr> {
427 use mdns_sd::{ServiceDaemon, ServiceEvent};
428 use std::time::Instant;
429 let mdns = ServiceDaemon::new().ok()?;
430 let rx = mdns.browse("_rlx-coord._udp.local.").ok()?;
431 let deadline = Instant::now() + timeout;
432 while let Some(left) = deadline.checked_duration_since(Instant::now())
433 && let Ok(ev) = rx.recv_timeout(left)
434 {
435 if let ServiceEvent::ServiceResolved(info) = ev
436 && let Some(ip) = info.get_addresses().iter().next()
437 {
438 return Some(SocketAddr::new(*ip, info.get_port()));
439 }
440 }
441 None
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447 use crate::collective::ReduceKind;
448
449 #[test]
450 fn node_mesh_connect_and_all_reduce() {
451 let free = || {
454 let l = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
455 l.local_addr().unwrap()
456 };
457 let (a0, a1) = (free(), free());
458 let peers = vec![a0.to_string(), a1.to_string()];
459
460 let p1 = peers.clone();
461 let h = std::thread::spawn(move || {
462 let g = Node::new(1, 2)
463 .peers(p1.iter().map(String::as_str))
464 .unwrap()
465 .connect()
466 .unwrap();
467 let mut d = vec![2.0f32; 3];
468 g.all_reduce(&mut d, ReduceKind::Sum).unwrap();
469 assert_eq!(d, vec![3.0; 3]);
470 g.barrier().unwrap();
471 });
472 let g = Node::new(0, 2)
473 .peers(peers.iter().map(String::as_str))
474 .unwrap()
475 .connect()
476 .unwrap();
477 let mut d = vec![1.0f32; 3];
478 g.all_reduce(&mut d, ReduceKind::Sum).unwrap();
479 assert_eq!(d, vec![3.0; 3]); g.barrier().unwrap();
481 h.join().unwrap();
482 }
483}