Skip to main content

nuvec_distributed/
lib.rs

1//! Distributed backends for nuvec: MLX's TCP ring.
2//!
3//! [`init`] reads MLX's launch environment (`MLX_HOSTFILE`, a JSON list with one entry per
4//! rank of `"host:port"` addresses, and `MLX_RANK`), connects the ring and makes it the
5//! default group of `nuvec::distributed`'s collectives. Every rank runs the same program;
6//! the processes may share one Mac (over loopback) or run on several (over Ethernet or
7//! Thunderbolt IP).
8//!
9//! ```no_run
10//! use nuvec_core::{array, distributed};
11//!
12//! # fn main() -> nuvec_core::Result<()> {
13//! let group = nuvec_distributed::init(false)?; // size 1 without a hostfile
14//! let x = array![1.0, 2.0];
15//! let total = distributed::all_sum(&x, None); // the sum over every rank
16//! println!("rank {} of {}: {:?}", group.rank(), group.size(), total.to_vec::<f32>()?);
17//! # Ok(())
18//! # }
19//! ```
20//!
21//! **Stability:** no promise when this crate is depended on directly; depend on `nuvec`,
22//! whose re-exports are semver-tracked.
23
24#![forbid(unsafe_code)]
25
26mod hostfile;
27mod ring;
28
29use std::sync::Arc;
30use std::time::Duration;
31
32use nuvec_core::distributed::{Group, set_default_group};
33use nuvec_core::{Error, Result};
34
35/// Options of [`init_with`].
36#[derive(Clone, Debug)]
37#[non_exhaustive]
38pub struct Options {
39    /// How long connecting and every transfer may wait for a peer before failing with an
40    /// error naming it. Default 30 s (`NUVEC_DISTRIBUTED_TIMEOUT`, in seconds, for
41    /// [`init`]).
42    pub timeout: Duration,
43}
44
45impl Default for Options {
46    fn default() -> Self {
47        Self {
48            timeout: Duration::from_secs(30),
49        }
50    }
51}
52
53impl Options {
54    /// Sets the timeout.
55    pub fn timeout(mut self, timeout: Duration) -> Self {
56        self.timeout = timeout;
57        self
58    }
59}
60
61/// Initialises the ring from MLX's environment (`MLX_HOSTFILE`, `MLX_RANK`) and makes it
62/// the default group, as `mx.distributed.init()`. Without the environment: an error if
63/// `strict`, else the group of this process alone.
64pub fn init(strict: bool) -> Result<Group> {
65    let (hostfile, rank) = (std::env::var("MLX_HOSTFILE"), std::env::var("MLX_RANK"));
66    let (Ok(hostfile), Ok(rank)) = (hostfile, rank) else {
67        if strict {
68            return Err(Error::InvalidArgument(
69                "distributed: MLX_HOSTFILE and MLX_RANK are not set".into(),
70            ));
71        }
72        return Ok(Group::single());
73    };
74    let text = std::fs::read_to_string(&hostfile)
75        .map_err(|e| Error::Io(format!("distributed: reading {hostfile}: {e}")))?;
76    let rank = rank
77        .trim()
78        .parse()
79        .map_err(|_| Error::InvalidArgument(format!("distributed: MLX_RANK {rank:?}")))?;
80    let mut options = Options::default();
81    if let Ok(secs) = std::env::var("NUVEC_DISTRIBUTED_TIMEOUT") {
82        let invalid = || {
83            Error::InvalidArgument(format!(
84                "distributed: NUVEC_DISTRIBUTED_TIMEOUT {secs:?} (seconds, a positive number)"
85            ))
86        };
87        let parsed: f64 = secs.trim().parse().map_err(|_| invalid())?;
88        options.timeout = Duration::try_from_secs_f64(parsed)
89            .ok()
90            .filter(|d| !d.is_zero())
91            .ok_or_else(invalid)?;
92    }
93    let group = init_with(&hostfile::parse(&text)?, rank, &options)?;
94    set_default_group(group.clone());
95    Ok(group)
96}
97
98/// Connects rank `rank` of the ring over `nodes` (per rank its addresses, one connection
99/// per address, as MLX's hostfile) and returns its group (not made the default). A single
100/// rank is the group of this process alone.
101pub fn init_with(nodes: &[Vec<String>], rank: usize, options: &Options) -> Result<Group> {
102    if nodes.len() <= 1 {
103        return Ok(Group::single());
104    }
105    let ring = ring::Ring::connect(nodes, rank, options.timeout)?;
106    Ok(Group::new(Arc::new(ring)))
107}