1use std::{
37 collections::HashMap,
38 io::{BufRead, BufReader, Read, Write},
39 net::{SocketAddr, TcpListener, TcpStream},
40 sync::{
41 atomic::{AtomicBool, Ordering},
42 Arc, Mutex,
43 },
44 thread,
45 time::Duration,
46};
47
48use serde_json::{json, Value};
49
50use crate::layout::{contract::Contract, route::validate_stage_fields};
51
52const MAX_REQUEST_BYTES: usize = 4 << 20;
54
55const READ_TIMEOUT: Duration = Duration::from_secs(10);
57
58const ACCEPT_POLL: Duration = Duration::from_millis(50);
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum Registry {
64 Addresses,
66 CacheLayouts,
69}
70
71#[derive(Debug, Clone)]
73struct RankAddress {
74 ip: Value,
75 port: i64,
76}
77
78#[derive(Debug, Default)]
80struct Table {
81 world_size: Option<Value>,
82 dp_size: Option<Value>,
83 tp_size_per_dp_rank: Option<i64>,
84 pp_size: Option<i64>,
85 ranks: HashMap<i64, HashMap<i64, RankAddress>>,
87 cache_layout: Option<(String, Contract)>,
89 cache_fields_by_stage: Option<Vec<Vec<String>>>,
90}
91
92struct Refusal(u16, String);
94
95fn internal(message: impl Into<String>) -> Refusal {
96 Refusal(500, message.into())
97}
98
99fn integer(value: Option<&Value>, name: &str) -> Result<i64, Refusal> {
101 let value = value.ok_or_else(|| internal(format!("missing {name}")))?;
102 value
103 .as_i64()
104 .or_else(|| value.as_str().and_then(|text| text.trim().parse().ok()))
105 .ok_or_else(|| internal(format!("{name} is not an integer")))
106}
107
108impl Table {
109 fn ingest_cache_layout(&mut self, data: &Value) -> Result<(), Refusal> {
113 let wire = data
114 .get("cache_layout")
115 .and_then(Value::as_str)
116 .filter(|wire| wire.is_ascii())
117 .ok_or_else(|| internal("CachePD bootstrap layout must be a string"))?;
118 let layout = Contract::from_wire(wire.as_bytes())
119 .map_err(|_| internal("CachePD bootstrap layout is invalid"))?;
120 let stages = data
121 .get("cache_fields_by_stage")
122 .and_then(Value::as_array)
123 .ok_or_else(|| internal("CachePD requires explicit stage field placement"))?
124 .iter()
125 .map(|stage| {
126 stage
127 .as_array()
128 .ok_or_else(|| internal("CachePD requires explicit stage field placement"))?
129 .iter()
130 .map(|field| {
131 field.as_str().map(str::to_owned).ok_or_else(|| {
132 internal(
133 "cache stage placement must cover every logical field exactly once",
134 )
135 })
136 })
137 .collect::<Result<Vec<_>, _>>()
138 })
139 .collect::<Result<Vec<_>, _>>()?;
140 validate_stage_fields(&layout, &stages).map_err(|error| internal(error.0))?;
141 if stages.len() as i64 != integer(data.get("pp_size"), "pp_size")? {
142 return Err(internal(
143 "CachePD placement does not match pipeline stage count",
144 ));
145 }
146 if self
147 .cache_fields_by_stage
148 .as_ref()
149 .is_some_and(|known| *known != stages)
150 {
151 return Err(internal(
152 "CachePD prefill ranks registered incompatible field placement",
153 ));
154 }
155 if self
156 .cache_layout
157 .as_ref()
158 .is_some_and(|(_, known)| *known != layout)
159 {
160 return Err(internal(
161 "CachePD prefill ranks registered incompatible layouts",
162 ));
163 }
164 if self.cache_layout.is_none() {
165 self.cache_layout = Some((wire.to_owned(), layout));
166 }
167 self.cache_fields_by_stage = Some(stages);
168 Ok(())
169 }
170
171 fn register(&mut self, registry: Registry, body: &[u8]) -> Result<(), Refusal> {
173 let data: Value =
174 serde_json::from_slice(body).map_err(|_| internal("the registration is not JSON"))?;
175 let field = |key: &str| {
176 data.get(key)
177 .cloned()
178 .ok_or_else(|| internal(format!("missing {key}")))
179 };
180 let role = field("role")?;
181 let world_size = field("world_size")?;
182 let dp_size = field("dp_size")?;
183 let ip = field("rank_ip")?;
184 let port = integer(data.get("rank_port"), "rank_port")?;
185 let engine_rank = integer(data.get("engine_rank"), "engine_rank")?;
186 if registry == Registry::CacheLayouts {
187 self.ingest_cache_layout(&data)?;
188 }
189 self.world_size.get_or_insert_with(|| world_size.clone());
190 self.dp_size.get_or_insert_with(|| dp_size.clone());
191 if let Some(pp_size) = data.get("pp_size") {
192 self.pp_size = Some(integer(Some(pp_size), "pp_size")?);
193 }
194 let world = integer(Some(&world_size), "world_size")?;
197 let dp = integer(Some(&dp_size), "dp_size")?;
198 if dp == 0 {
199 return Err(internal("dp_size must not be zero"));
200 }
201 let tp_size_per_dp_rank = world.div_euclid(dp);
202 self.tp_size_per_dp_rank.get_or_insert(tp_size_per_dp_rank);
203 if role.as_str() == Some("Prefill") {
204 if tp_size_per_dp_rank == 0 {
205 return Err(internal("world_size must not be below dp_size"));
206 }
207 let dp_group = engine_rank.div_euclid(tp_size_per_dp_rank);
208 let tp_rank = engine_rank.rem_euclid(tp_size_per_dp_rank);
209 self.ranks
210 .entry(dp_group)
211 .or_default()
212 .insert(tp_rank, RankAddress { ip, port });
213 tracing::debug!(engine_rank, port, "a source rank registered");
214 }
215 Ok(())
216 }
217
218 fn route(&self, registry: Registry, query: &HashMap<String, String>) -> Result<Value, Refusal> {
220 let (Some(engine_rank), Some(dp_group)) = (
221 query.get("engine_rank").filter(|value| !value.is_empty()),
222 query
223 .get("target_dp_group")
224 .filter(|value| !value.is_empty()),
225 ) else {
226 return Err(Refusal(
227 400,
228 "Missing inputs for bootstrap server.".to_owned(),
229 ));
230 };
231 let parse = |value: &str, name: &str| {
232 value
233 .trim()
234 .parse::<i64>()
235 .map_err(|_| internal(format!("{name} is not an integer")))
236 };
237 let engine_rank = parse(engine_rank, "engine_rank")?;
238 let dp_group = parse(dp_group, "target_dp_group")?;
239 if engine_rank == -1 && dp_group == -1 {
240 let mut info = json!({
241 "prefill_tp_size": self.world_size.clone().unwrap_or(Value::Null),
242 "prefill_dp_size": self.dp_size.clone().unwrap_or(Value::Null),
243 "prefill_pp_size": self.pp_size.unwrap_or(1),
244 });
245 if registry == Registry::CacheLayouts {
246 info["cache_layout"] = self
247 .cache_layout
248 .as_ref()
249 .map_or(Value::Null, |(wire, _)| Value::from(wire.as_str()));
250 info["cache_fields_by_stage"] = self
251 .cache_fields_by_stage
252 .as_ref()
253 .map_or(Value::Null, |stages| json!(stages));
254 }
255 return Ok(info);
256 }
257 let address = self
258 .ranks
259 .get(&dp_group)
260 .and_then(|ranks| ranks.get(&engine_rank))
261 .ok_or_else(|| internal("Bootstrap info not Found"))?;
262 Ok(json!({"rank_ip": address.ip, "rank_port": address.port}))
263 }
264}
265
266struct Request {
268 method: String,
269 path: String,
270 query: HashMap<String, String>,
271 body: Vec<u8>,
272}
273
274fn percent_decode(text: &str) -> String {
276 let bytes = text.as_bytes();
277 let mut out = Vec::with_capacity(bytes.len());
278 let mut index = 0;
279 while index < bytes.len() {
280 let escaped = (bytes[index] == b'%')
281 .then(|| bytes.get(index + 1..index + 3))
282 .flatten()
283 .and_then(|hex| std::str::from_utf8(hex).ok())
284 .and_then(|hex| u8::from_str_radix(hex, 16).ok());
285 match (escaped, bytes[index]) {
286 (Some(byte), _) => {
287 out.push(byte);
288 index += 3;
289 continue;
290 }
291 (None, b'+') => out.push(b' '),
292 (None, byte) => out.push(byte),
293 }
294 index += 1;
295 }
296 String::from_utf8_lossy(&out).into_owned()
297}
298
299fn read_request(stream: &TcpStream) -> Option<Request> {
300 let mut reader = BufReader::new(stream.take(MAX_REQUEST_BYTES as u64));
301 let mut line = String::new();
302 reader.read_line(&mut line).ok()?;
303 let mut parts = line.split_whitespace();
304 let method = parts.next()?.to_owned();
305 let target = parts.next()?.to_owned();
306 let mut length = 0usize;
307 loop {
308 let mut header = String::new();
309 if reader.read_line(&mut header).ok()? == 0 {
310 break;
311 }
312 let header = header.trim_end();
313 if header.is_empty() {
314 break;
315 }
316 if let Some((name, value)) = header.split_once(':') {
317 if name.trim().eq_ignore_ascii_case("content-length") {
318 length = value.trim().parse().ok()?;
319 }
320 }
321 }
322 if length > MAX_REQUEST_BYTES {
323 return None;
324 }
325 let mut body = vec![0; length];
326 reader.read_exact(&mut body).ok()?;
327 let (path, query) = target.split_once('?').unwrap_or((target.as_str(), ""));
328 let query = query
329 .split('&')
330 .filter(|pair| !pair.is_empty())
331 .map(|pair| {
332 let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
333 (percent_decode(key), percent_decode(value))
334 })
335 .collect();
336 Some(Request {
337 method,
338 path: path.to_owned(),
339 query,
340 body,
341 })
342}
343
344fn reply(mut stream: &TcpStream, status: u16, content_type: &str, body: &str) {
345 let reason = match status {
346 200 => "OK",
347 400 => "Bad Request",
348 404 => "Not Found",
349 405 => "Method Not Allowed",
350 _ => "Internal Server Error",
351 };
352 let _ = write!(
353 stream,
354 "HTTP/1.1 {status} {reason}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\n\
355 Connection: close\r\n\r\n{body}",
356 body.len()
357 );
358 let _ = stream.flush();
359}
360
361fn serve(stream: TcpStream, registry: Registry, table: &Mutex<Table>) {
362 let _ = stream.set_read_timeout(Some(READ_TIMEOUT));
363 let Some(request) = read_request(&stream) else {
364 reply(&stream, 400, "text/plain", "Bad Request");
365 return;
366 };
367 let answer = match (request.path.as_str(), request.method.as_str()) {
368 ("/health", "GET") => Ok(None),
369 ("/route", "PUT") => table
370 .lock()
371 .expect("the bootstrap table lock is poisoned")
372 .register(registry, &request.body)
373 .map(|()| None),
374 ("/route", "GET") => table
375 .lock()
376 .expect("the bootstrap table lock is poisoned")
377 .route(registry, &request.query)
378 .map(Some),
379 ("/route" | "/health", _) => Err(Refusal(405, "Method not allowed".to_owned())),
380 _ => Err(Refusal(404, "Not Found".to_owned())),
381 };
382 match answer {
383 Ok(None) => reply(&stream, 200, "text/plain", "OK"),
384 Ok(Some(body)) => reply(&stream, 200, "application/json", &body.to_string()),
385 Err(Refusal(status, message)) => {
386 if status == 500 {
387 tracing::error!(
388 path = request.path,
389 message,
390 "the bootstrap server refused a request"
391 );
392 }
393 reply(&stream, status, "text/plain", &message);
394 }
395 }
396}
397
398fn bind(port: u16) -> std::io::Result<TcpListener> {
401 TcpListener::bind(SocketAddr::from(([0u16; 8], port)))
402 .or_else(|_| TcpListener::bind(SocketAddr::from(([0u8; 4], port))))
403}
404
405pub struct BootstrapServer {
407 port: u16,
408 stop: Arc<AtomicBool>,
409 thread: Option<thread::JoinHandle<()>>,
410}
411
412impl BootstrapServer {
413 pub fn start(port: u16, registry: Registry) -> std::io::Result<Self> {
415 let listener = bind(port)?;
416 let port = listener.local_addr()?.port();
417 listener.set_nonblocking(true)?;
418 let stop = Arc::new(AtomicBool::new(false));
419 let table = Arc::new(Mutex::new(Table::default()));
420 let thread = {
421 let stop = stop.clone();
422 thread::Builder::new()
423 .name("ts-bootstrap".to_owned())
424 .spawn(move || {
425 while !stop.load(Ordering::Relaxed) {
426 match listener.accept() {
427 Ok((stream, _)) => {
428 let table = table.clone();
429 let _ = stream.set_nonblocking(false);
430 let spawned = thread::Builder::new()
431 .name("ts-bootstrap-conn".to_owned())
432 .spawn(move || serve(stream, registry, &table));
433 if let Err(error) = spawned {
434 tracing::error!(%error, "cannot serve a bootstrap connection");
435 }
436 }
437 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
438 thread::sleep(ACCEPT_POLL);
439 }
440 Err(error) => {
441 tracing::warn!(%error, "the bootstrap server failed to accept");
442 thread::sleep(ACCEPT_POLL);
443 }
444 }
445 }
446 })?
447 };
448 tracing::info!(port, ?registry, "bootstrap server up");
449 Ok(Self {
450 port,
451 stop,
452 thread: Some(thread),
453 })
454 }
455
456 pub fn port(&self) -> u16 {
458 self.port
459 }
460
461 pub fn close(&mut self) {
463 self.stop.store(true, Ordering::Relaxed);
464 if let Some(thread) = self.thread.take() {
465 let _ = thread.join();
466 }
467 }
468}
469
470impl Drop for BootstrapServer {
471 fn drop(&mut self) {
472 self.close();
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use crate::http;
480
481 const TIMEOUT: Duration = Duration::from_secs(5);
482
483 fn url(server: &BootstrapServer, target: &str) -> String {
484 format!("http://127.0.0.1:{}{target}", server.port())
485 }
486
487 fn registration(engine_rank: i64, port: i64) -> Value {
488 json!({
489 "role": "Prefill",
490 "world_size": 4,
491 "dp_size": 2,
492 "rank_ip": "10.0.0.1",
493 "rank_port": port,
494 "engine_rank": engine_rank,
495 })
496 }
497
498 #[test]
499 fn source_ranks_register_and_are_looked_up() {
500 let mut server = BootstrapServer::start(0, Registry::Addresses).unwrap();
501 assert!(http::healthy(&url(&server, "/health"), TIMEOUT, TIMEOUT));
502 for (rank, port) in [(0, 7000), (3, 7003)] {
503 http::put_json(&url(&server, "/route"), ®istration(rank, port), TIMEOUT).unwrap();
504 }
505 let info: Value = serde_json::from_str(
506 &http::get_text(
507 &url(&server, "/route?engine_rank=-1&target_dp_group=-1"),
508 TIMEOUT,
509 )
510 .unwrap(),
511 )
512 .unwrap();
513 assert_eq!(
514 info,
515 json!({"prefill_tp_size": 4, "prefill_dp_size": 2, "prefill_pp_size": 1})
516 );
517 let rank: Value = serde_json::from_str(
519 &http::get_text(
520 &url(&server, "/route?engine_rank=1&target_dp_group=1"),
521 TIMEOUT,
522 )
523 .unwrap(),
524 )
525 .unwrap();
526 assert_eq!(rank, json!({"rank_ip": "10.0.0.1", "rank_port": 7003}));
527 let missing = http::get_text(
528 &url(&server, "/route?engine_rank=1&target_dp_group=0"),
529 TIMEOUT,
530 );
531 assert!(missing.unwrap_err().starts_with("500"));
532 let incomplete = http::get_text(&url(&server, "/route?engine_rank=1"), TIMEOUT);
533 assert!(incomplete.unwrap_err().starts_with("400"));
534 server.close();
535 assert!(!http::healthy(&url(&server, "/health"), TIMEOUT, TIMEOUT));
536 }
537
538 fn layout(page_count: i64) -> String {
539 json!({
540 "plan": {
541 "prefix_granularity": 16,
542 "lcm_block_bytes": 64,
543 "num_lcm_blocks": page_count - 1,
544 "groups": [{"group_id": "kv", "cache_blocks_per_lcm_block": 1,
545 "page_count": page_count}],
546 "planes": [{"plane_id": "p0", "bytes_per_lcm_block": 64, "arena_offset_bytes": 0}],
547 "fields": [
548 {"group_id": "kv", "field_id": "layer.0.kv", "plane_id": "p0", "shape": [16],
549 "dtype": "bfloat16", "field_offset_bytes": 0, "page_stride_bytes": 64},
550 {"group_id": "kv", "field_id": "layer.1.kv", "plane_id": "p0", "shape": [16],
551 "dtype": "bfloat16", "field_offset_bytes": 32, "page_stride_bytes": 64}
552 ],
553 "resident_block_bytes": null
554 },
555 "group_specs": [{"group_id": "kv", "attn_kind": "full_history", "rows_per_page": 16,
556 "entry_stride_tokens": 1, "sliding_window_tokens": null,
557 "replayable": false, "transfer_policy": "full_suffix",
558 "shard_count": 1}],
559 "transfer_schema": {"fields": []},
560 "field_addresses": null
561 })
562 .to_string()
563 }
564
565 fn cache_registration(engine_rank: i64, layout: &str, stages: Value) -> Value {
566 let mut data = registration(engine_rank, 7000 + engine_rank);
567 data["pp_size"] = json!(stages.as_array().unwrap().len());
568 data["cache_layout"] = json!(layout);
569 data["cache_fields_by_stage"] = stages;
570 data
571 }
572
573 #[test]
574 fn a_cache_transfer_node_registers_one_layout_and_placement() {
575 let server = BootstrapServer::start(0, Registry::CacheLayouts).unwrap();
576 let route = url(&server, "/route");
577 let stages = json!([["layer.0.kv"], ["layer.1.kv"]]);
578 http::put_json(
579 &route,
580 &cache_registration(0, &layout(8), stages.clone()),
581 TIMEOUT,
582 )
583 .unwrap();
584 for refused in [
587 cache_registration(1, &layout(9), stages.clone()),
588 cache_registration(1, &layout(8), json!([["layer.0.kv"]])),
589 cache_registration(1, &layout(8), json!([["layer.0.kv", "layer.1.kv"]])),
590 ] {
591 let error = http::put_json(&route, &refused, TIMEOUT).unwrap_err();
592 assert!(error.starts_with("500"), "{error}");
593 }
594 let mut wrong_pp = cache_registration(1, &layout(8), stages.clone());
595 wrong_pp["pp_size"] = json!(1);
596 assert!(http::put_json(&route, &wrong_pp, TIMEOUT).is_err());
597 let lookup = url(&server, "/route?engine_rank=1&target_dp_group=0");
598 assert!(http::get_text(&lookup, TIMEOUT).is_err());
599
600 http::put_json(
601 &route,
602 &cache_registration(1, &layout(8), stages.clone()),
603 TIMEOUT,
604 )
605 .unwrap();
606 let info: Value = serde_json::from_str(
607 &http::get_text(
608 &url(&server, "/route?engine_rank=-1&target_dp_group=-1"),
609 TIMEOUT,
610 )
611 .unwrap(),
612 )
613 .unwrap();
614 assert_eq!(info["prefill_pp_size"], json!(2));
615 assert_eq!(info["cache_fields_by_stage"], stages);
616 let served =
617 Contract::from_wire(info["cache_layout"].as_str().unwrap().as_bytes()).unwrap();
618 assert_eq!(served, Contract::from_wire(layout(8).as_bytes()).unwrap());
619 assert!(http::get_text(&lookup, TIMEOUT).is_ok());
620 }
621
622 #[test]
623 fn a_taken_port_is_refused() {
624 let server = BootstrapServer::start(0, Registry::Addresses).unwrap();
625 assert!(BootstrapServer::start(server.port(), Registry::Addresses).is_err());
626 }
627}