spark_storage/weight_peer/
serve.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//
3// The `atlas-weight-peer` daemon (unix, server-side). Accepts a model request,
4// stages the model (warm mmaps + parsed manifest via `shard`), publishes the
5// manifest (`wire`), then serves the shards one-sided REMOTE_READ over verbs.
6// This module holds the sole `reg_mr(.., true)` — the REMOTE_READ registration
7// pinned by `tests/reg_mr_flag_audit.rs`.
8
9use anyhow::{Context, Result, bail};
10use std::collections::HashMap;
11use std::io::Read;
12use std::net::{TcpListener, TcpStream, ToSocketAddrs};
13use std::path::{Path, PathBuf};
14use std::sync::{Arc, Mutex};
15
16use super::manifest::WeightManifest;
17use super::shard::{Mmap, build_manifest};
18use super::wire::{read_model_request, write_weight_manifest};
19use crate::expert_peer::MODE_VERBS;
20
21/// Peer configuration: the RDMA rails, the memory ceiling, and how the peer
22/// resolves a model request to a directory.
23#[derive(Clone, Debug)]
24pub struct WeightPeerConfig {
25    /// `(device, gid_idx)` per rail, in link order (rail 0 = the cabled
26    /// link). Mirrors `expert_peer::RdmaConfig::rails`.
27    pub rails: Vec<(String, u32)>,
28    /// Ceiling on total registered (staged) RAM in bytes across ALL staged
29    /// models. `0` = unlimited. Each model is charged its shard bytes once,
30    /// at first stage (the per-connection MRs share the same warm pages).
31    pub max_blade_bytes: u64,
32    /// Model directories to pre-stage at startup. Also the allow-list when
33    /// `allow_any_path` is false: a client may only request a model whose
34    /// resolved path matches one of these (or its basename).
35    pub staged_dirs: Vec<PathBuf>,
36    /// When true, a client may request ANY filesystem path and the peer
37    /// stages it on demand (convenient for a trusted LAN; off by default).
38    pub allow_any_path: bool,
39}
40
41impl Default for WeightPeerConfig {
42    fn default() -> Self {
43        Self {
44            rails: vec![("roceP2p1s0f1".into(), 3)],
45            max_blade_bytes: 0,
46            staged_dirs: Vec::new(),
47            allow_any_path: false,
48        }
49    }
50}
51
52/// A staged model held resident: the persistent shard mmaps (kept mapped so
53/// their pages stay warm in RAM across connections), the manifest, and the
54/// ledger reservation released when the model is dropped. Per-connection
55/// `reg_mr` re-registers these same base VAs on each client QP's PD.
56struct StagedModel {
57    // Read only by the verbs serve path (reg_mr each shard); on a build
58    // without rdma-core the mmaps still hold pages warm but aren't iterated.
59    #[cfg_attr(not(atlas_rdma_verbs), allow(dead_code))]
60    shard_mmaps: Vec<Mmap>,
61    manifest: WeightManifest,
62    _reservation: crate::blade_cap::Reservation,
63}
64
65type StagedMap = Arc<Mutex<HashMap<String, Arc<StagedModel>>>>;
66
67/// Serve staged models on `addr` until interrupted. One thread per
68/// connection; blocking. Intended to run as its own process
69/// (`atlas-weight-peer`).
70pub fn serve<A: ToSocketAddrs>(addr: A, cfg: WeightPeerConfig) -> Result<()> {
71    let cfg = Arc::new(cfg);
72    let ledger = Arc::new(crate::blade_cap::CommitLedger::new(cfg.max_blade_bytes));
73    let staged: StagedMap = Arc::new(Mutex::new(HashMap::new()));
74
75    // Pre-stage the configured directories (first stage is the slow one —
76    // do it up front so the first client swap is already warm).
77    for dir in &cfg.staged_dirs {
78        match stage_model(&staged, &ledger, dir) {
79            Ok(m) => tracing::info!(
80                "weight-peer pre-staged {} ({} shards, {} tensors, {:.1} GiB)",
81                m.manifest.model_id,
82                m.manifest.num_shards(),
83                m.manifest.tensors.len(),
84                m.manifest.total_shard_bytes() as f64 / (1024.0 * 1024.0 * 1024.0),
85            ),
86            Err(e) => tracing::warn!("weight-peer pre-stage {} failed: {e}", dir.display()),
87        }
88    }
89
90    let listener = TcpListener::bind(addr).context("bind weight-peer listener")?;
91    let local = listener.local_addr().ok();
92    tracing::info!(
93        "weight-peer serving on {:?} (verbs rails {:?}, cap {}, allow_any_path {})",
94        local,
95        cfg.rails,
96        if cfg.max_blade_bytes == 0 {
97            "unlimited".to_string()
98        } else {
99            format!(
100                "{:.1} GiB",
101                cfg.max_blade_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
102            )
103        },
104        cfg.allow_any_path,
105    );
106
107    for conn in listener.incoming() {
108        let stream = match conn {
109            Ok(s) => s,
110            Err(e) => {
111                tracing::warn!("weight-peer accept error: {e}");
112                continue;
113            }
114        };
115        let cfg = cfg.clone();
116        let ledger = ledger.clone();
117        let staged = staged.clone();
118        std::thread::spawn(move || {
119            if let Err(e) = handle_conn(stream, &cfg, &ledger, &staged) {
120                tracing::warn!("weight-peer connection ended: {e}");
121            }
122        });
123    }
124    Ok(())
125}
126
127fn handle_conn(
128    mut stream: TcpStream,
129    cfg: &WeightPeerConfig,
130    ledger: &Arc<crate::blade_cap::CommitLedger>,
131    staged: &StagedMap,
132) -> Result<()> {
133    stream.set_nodelay(true).ok();
134
135    // 1. Client tells us which model it wants.
136    let request = read_model_request(&mut stream)?;
137    let dir = resolve_request(cfg, &request)?;
138
139    // 2. Stage it (or reuse the warm one) and publish the manifest.
140    let model = stage_model(staged, ledger, &dir)?;
141    write_weight_manifest(&mut stream, &model.manifest).context("send manifest")?;
142
143    // 3. Transport selection. Only verbs is served for weights.
144    let mut mode = [0u8; 1];
145    stream
146        .read_exact(&mut mode)
147        .context("read transport mode")?;
148    match mode[0] {
149        MODE_VERBS => serve_verbs(stream, &model, cfg),
150        other => bail!("weight-peer only serves verbs; client asked for mode {other}"),
151    }
152}
153
154/// Resolve a client's model request string to a directory, honoring the
155/// allow-list / `allow_any_path` policy.
156fn resolve_request(cfg: &WeightPeerConfig, request: &str) -> Result<PathBuf> {
157    let req = Path::new(request);
158    // Exact path match against a staged dir, or basename match.
159    for d in &cfg.staged_dirs {
160        if d == req
161            || d.file_name().and_then(|n| n.to_str()) == Some(request)
162            || d.to_string_lossy() == request
163        {
164            return Ok(d.clone());
165        }
166    }
167    if cfg.allow_any_path && req.is_dir() {
168        return Ok(req.to_path_buf());
169    }
170    bail!(
171        "model '{request}' is not staged (and allow_any_path is off); \
172         pass it to the peer with --stage <dir>"
173    );
174}
175
176/// Look up a warm staged model or stage it now (mmap shards + parse headers
177/// + charge the ledger). Idempotent per resolved-path key.
178fn stage_model(
179    staged: &StagedMap,
180    ledger: &Arc<crate::blade_cap::CommitLedger>,
181    dir: &Path,
182) -> Result<Arc<StagedModel>> {
183    let key = dir.to_string_lossy().into_owned();
184    {
185        let map = staged.lock().unwrap();
186        if let Some(m) = map.get(&key) {
187            return Ok(m.clone());
188        }
189    }
190
191    // Build the manifest (resolve shards + parse each shard's header) and
192    // mmap every shard REMOTE-read + warm.
193    let (shard_paths, manifest) = build_manifest(dir, &key)?;
194    // Charge the ledger BEFORE pinning any pages; the RAII guard lives in
195    // the StagedModel and releases if we bail below or when it's dropped.
196    let reservation = ledger
197        .try_reserve(manifest.total_shard_bytes())
198        .context("weight blade cap")?;
199
200    let mut shard_mmaps = Vec::with_capacity(shard_paths.len());
201    for p in &shard_paths {
202        shard_mmaps.push(Mmap::open_ro(p).with_context(|| format!("mmap {}", p.display()))?);
203    }
204
205    let model = Arc::new(StagedModel {
206        shard_mmaps,
207        manifest,
208        _reservation: reservation,
209    });
210    let mut map = staged.lock().unwrap();
211    // Another thread may have staged it while we worked; prefer the existing
212    // one (drops ours, releasing its reservation).
213    Ok(map.entry(key).or_insert(model).clone())
214}
215
216/// One-sided RDMA READ weight serving. Registers each shard mmap REMOTE_READ
217/// on every rail, publishes the per-shard `(base, rkey)`, connects to the
218/// client's QPs, then idles — the client pulls all tensor bytes one-sided.
219#[cfg(not(atlas_rdma_verbs))]
220fn serve_verbs(
221    _stream: TcpStream,
222    _model: &Arc<StagedModel>,
223    _cfg: &WeightPeerConfig,
224) -> Result<()> {
225    bail!("client requested verbs transport but this peer was built without rdma-core");
226}
227
228#[cfg(atlas_rdma_verbs)]
229fn serve_verbs(
230    mut stream: TcpStream,
231    model: &Arc<StagedModel>,
232    cfg: &WeightPeerConfig,
233) -> Result<()> {
234    use crate::expert_peer::{STATUS_OK, VerbsClientParams, VerbsServerParams, write_server_rails};
235    use atlas_rdma::verbs::Verbs;
236    use std::io::Write;
237
238    let num_shards = model.shard_mmaps.len();
239
240    // Negotiate the rail count.
241    let mut b1 = [0u8; 1];
242    stream.read_exact(&mut b1).context("read n_rails")?;
243    let n_rails = b1[0] as usize;
244    if n_rails == 0 || n_rails > cfg.rails.len() {
245        bail!(
246            "client asked for {n_rails} rails; peer has {}",
247            cfg.rails.len()
248        );
249    }
250
251    // One QP per rail (distinct per-rail PSN so successive clients don't
252    // collide). No ledger charge here — staging already charged the pages;
253    // the N per-rail MRs share those same refcounted mmap pages.
254    let pid = std::process::id();
255    let mut rails: Vec<Verbs> = Vec::with_capacity(n_rails);
256    for (i, (dev, gid)) in cfg.rails.iter().take(n_rails).enumerate() {
257        let psn = (0x77_7777 ^ pid ^ ((i as u32) << 20)) & 0xff_ffff;
258        rails.push(Verbs::create(dev, *gid, psn)?);
259    }
260
261    // Register each shard mmap (REMOTE_READ) on EVERY rail's PD — one rkey
262    // per (rail, shard), identical base VA, shared physical pages. The mmaps
263    // live in the persistent StagedModel, so they outlive these MRs.
264    let mut per_rail_shards: Vec<Vec<(u64, u32)>> = (0..n_rails)
265        .map(|_| Vec::with_capacity(num_shards))
266        .collect();
267    for m in &model.shard_mmaps {
268        for (ri, v) in rails.iter_mut().enumerate() {
269            // SAFETY: the mapping covers `m.len` bytes at `m.addr` and lives
270            // in the StagedModel Arc, which outlives every rail here.
271            let keys = unsafe { v.reg_mr(m.addr as *mut _, m.len, true)? };
272            per_rail_shards[ri].push((m.addr as u64, keys.rkey));
273        }
274    }
275
276    // Publish one VerbsServerParams per rail; `layers` carries per-SHARD
277    // (base, rkey) in shard order (shards play experts' per-layer role).
278    let sp: Vec<VerbsServerParams> = rails
279        .iter()
280        .enumerate()
281        .map(|(ri, v)| VerbsServerParams {
282            qpn: v.qpn(),
283            psn: v.psn(),
284            gid: v.gid(),
285            layers: std::mem::take(&mut per_rail_shards[ri]),
286        })
287        .collect();
288    write_server_rails(&mut stream, &sp).context("send verbs server params")?;
289
290    // Learn each client rail's QP, connect, ack.
291    stream.read_exact(&mut b1).context("read client n_rails")?;
292    if b1[0] as usize != n_rails {
293        bail!("client rail count mismatch");
294    }
295    for v in rails.iter_mut() {
296        let cp = VerbsClientParams::read_from(&mut stream).context("read verbs client params")?;
297        v.connect(cp.qpn, cp.psn, &cp.gid)?;
298    }
299    stream
300        .write_all(&[STATUS_OK])
301        .context("send verbs ready ack")?;
302    tracing::info!(
303        "weight-peer verbs client connected to {} ({n_rails} rail(s), {num_shards} shard MRs/rail)",
304        model.manifest.model_id,
305    );
306
307    // Idle until the client hangs up. All movement is one-sided RDMA READ.
308    let mut sink = [0u8; 8];
309    loop {
310        match stream.read(&mut sink) {
311            Ok(0) => break,
312            Ok(_) => {}
313            Err(_) => break,
314        }
315    }
316    // Drop rails (dereg MRs) BEFORE the StagedModel Arc frees anything — the
317    // mmaps persist in the map regardless, but dropping rails first keeps
318    // dereg strictly over live mappings.
319    drop(rails);
320    Ok(())
321}