ts_pd/lib.rs
1// Copyright (c) 2026 LightSeek Foundation
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! TokenSpeed's PD transfer peer (RFC 0002): one rank's half of moving a
22//! request's KV from the prefill node to the decode node.
23//!
24//! The peer owns the Mooncake transfer engine, the P/D handshake over ZMQ,
25//! the bootstrap-server HTTP calls, the room states, the transfer workers
26//! and the per-round agreement on transfer progress. It also plans
27//! everything that needs the ranks' cache layouts ([`layout`]): the routes
28//! between unequal Prefill and Decode rank sets, the block manifests read
29//! off the plans' block tables, and the copies one transfer makes. The data
30//! plane only builds the rank's cache transfer contract at startup and
31//! tells the forward thread when a plan's remote stream may go out
32//! ([`PrefillOutbox`], [`DecodeOutbox`]). The bootstrap server the nodes
33//! rendezvous through is here too ([`bootstrap`]). The wire formats are
34//! unchanged from the Python peers this crate replaced, so nodes of either
35//! version interoperate.
36
37mod batch;
38pub mod bootstrap;
39mod control;
40mod decode;
41pub mod epd;
42mod http;
43pub mod layout;
44mod net;
45mod prefill;
46mod status;
47mod wire;
48
49use std::{
50 path::PathBuf,
51 sync::{Arc, RwLock},
52 time::Duration,
53};
54
55pub use batch::BatchRows;
56pub use bootstrap::{BootstrapServer, Registry};
57pub use control::{
58 BootstrapInfo, EmbeddingChunk, EmbeddingItem, EmbeddingReceiver, EmbeddingSender, LandedItem,
59 LandedRequest, PdError, PdEvent, RemotePrefill, TransferPeer,
60};
61pub use decode::{DecodeConfig, DecodeOutbox, DecodePeer};
62pub use layout::{
63 registration::{PrefillLayout, PrefillRoute},
64 LayoutError,
65};
66pub use net::NetError;
67pub use prefill::{Layerwise, PrefillConfig, PrefillOutbox, PrefillPeer};
68pub use status::TransferPoll;
69use ts_mooncake::{Engine, PageGrid};
70pub use wire::StatusMessage;
71
72/// What one transfer copies, in order.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum Copy {
75 /// A pages x fields grid.
76 Pages(PageGrid),
77 /// Contiguous spans: source, destination, length each.
78 Spans {
79 /// Local addresses.
80 sources: Vec<u64>,
81 /// The peer's addresses.
82 destinations: Vec<u64>,
83 /// Byte counts.
84 lengths: Vec<u64>,
85 },
86}
87
88/// The most descriptors one WRITE batch carries.
89const DESCRIPTOR_BATCH: usize = 4096;
90
91/// A peer that failed to start.
92#[derive(Debug, thiserror::Error)]
93pub enum StartError {
94 /// The transfer engine failed.
95 #[error(transparent)]
96 Engine(#[from] ts_mooncake::Error),
97 /// The control sockets failed.
98 #[error(transparent)]
99 Net(#[from] NetError),
100 /// A thread did not start.
101 #[error("starting a PD thread: {0}")]
102 Thread(#[from] std::io::Error),
103 /// The rank's cache transfer contract is malformed.
104 #[error(transparent)]
105 Layout(#[from] LayoutError),
106}
107
108/// What both roles start from.
109#[derive(Debug, Clone)]
110pub struct PeerConfig {
111 /// The Mooncake library (`libtokenspeed_transfer_engine.so`).
112 pub library: PathBuf,
113 /// This rank's address peers reach it at (`get_local_ip_by_remote`).
114 pub local_ip: String,
115 /// The RDMA devices to use, comma-separated; empty for all.
116 pub ib_device: String,
117 /// The device ranges to register: the cache arena or its regions.
118 pub regions: Vec<(usize, usize)>,
119 /// This rank's global rank, for messages.
120 pub engine_rank: i64,
121 /// The transfer metrics, when the engine exports metrics.
122 pub metrics: Option<Arc<ts_metrics::KvTransferMetrics>>,
123}
124
125/// The engine and sockets both roles share.
126struct Transport {
127 library: Arc<ts_mooncake::Library>,
128 local_ip: String,
129 ib_device: String,
130 /// The device ranges registered with the engine.
131 regions: Vec<(usize, usize)>,
132 /// Replaced when a sleeping Decode rank wakes under a fresh session.
133 engine: RwLock<Arc<Engine>>,
134 sockets: net::Sockets,
135}
136
137impl Transport {
138 fn start(
139 config: &PeerConfig,
140 incoming: std::sync::mpsc::Sender<Vec<bytes::Bytes>>,
141 ) -> Result<Arc<Self>, StartError> {
142 let library = ts_mooncake::Library::load(&config.library)?;
143 let engine = Engine::start(library.clone(), &config.local_ip, &config.ib_device)?;
144 for &(addr, length) in &config.regions {
145 engine.register(addr, length)?;
146 }
147 tracing::info!(
148 session = engine.session_id(),
149 ib_device = config.ib_device,
150 "Mooncake transfer engine ready"
151 );
152 let sockets = net::Sockets::start(&config.local_ip, incoming)?;
153 Ok(Arc::new(Self {
154 library,
155 local_ip: config.local_ip.clone(),
156 ib_device: config.ib_device.clone(),
157 regions: config.regions.clone(),
158 engine: RwLock::new(Arc::new(engine)),
159 sockets,
160 }))
161 }
162
163 /// The engine transfers run on.
164 fn engine(&self) -> Arc<Engine> {
165 self.engine
166 .read()
167 .expect("the engine lock is poisoned")
168 .clone()
169 }
170
171 /// The id peers address this rank's memory by.
172 fn session_id(&self) -> String {
173 self.engine().session_id().to_owned()
174 }
175
176 /// Deregister the cache before a sleep unmaps its memory.
177 fn release_regions(&self) -> Result<(), String> {
178 let engine = self.engine();
179 let failed = self
180 .regions
181 .iter()
182 .filter(|&&(addr, _)| engine.unregister(addr).is_err())
183 .count();
184 if failed > 0 {
185 return Err(format!(
186 "Mooncake failed to deregister {failed} cache region(s) before the memory release"
187 ));
188 }
189 Ok(())
190 }
191
192 /// Register the re-mapped cache after a wake; with `fresh_session`
193 /// under a new engine, so no peer keeps writing with the keys of the
194 /// pages the sleep freed.
195 fn restore_regions(&self, fresh_session: bool) -> Result<(), String> {
196 let engine = if fresh_session {
197 Arc::new(
198 Engine::start(self.library.clone(), &self.local_ip, &self.ib_device)
199 .map_err(|error| error.to_string())?,
200 )
201 } else {
202 self.engine()
203 };
204 let failed = self
205 .regions
206 .iter()
207 .filter(|&&(addr, length)| engine.register(addr, length).is_err())
208 .count();
209 if failed > 0 {
210 return Err(format!(
211 "Mooncake failed to register {failed} cache region(s) after the memory resume"
212 ));
213 }
214 if fresh_session {
215 tracing::info!(session = engine.session_id(), "a fresh transfer session");
216 *self.engine.write().expect("the engine lock is poisoned") = engine;
217 }
218 Ok(())
219 }
220
221 /// Run one transfer's copies to `session`.
222 fn write(&self, session: &str, copies: &[Copy]) -> Result<(), ts_mooncake::Error> {
223 let engine = self.engine();
224 for copy in copies {
225 match copy {
226 Copy::Pages(grid) => engine.write_pages(session, grid, DESCRIPTOR_BATCH)?,
227 Copy::Spans {
228 sources,
229 destinations,
230 lengths,
231 } => {
232 for start in (0..sources.len()).step_by(DESCRIPTOR_BATCH) {
233 let end = (start + DESCRIPTOR_BATCH).min(sources.len());
234 engine.write(
235 session,
236 &sources[start..end],
237 &destinations[start..end],
238 &lengths[start..end],
239 )?;
240 }
241 }
242 }
243 }
244 Ok(())
245 }
246}
247
248/// Sleep `duration` unless `stop` fires first; returns whether it fired.
249fn sleep_or_stop(stop: &std::sync::mpsc::Receiver<()>, duration: Duration) -> bool {
250 !matches!(
251 stop.recv_timeout(duration),
252 Err(std::sync::mpsc::RecvTimeoutError::Timeout)
253 )
254}