1use std::collections::BTreeMap;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::{Arc, Mutex};
6
7use superfluid_linkf::Liveness;
8use superfluid_proto::linkf::Lease;
9
10use crate::fleet::{FleetGeneration, FleetHead};
11use crate::wal::GenParams;
12use crate::DaemonError;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum PlacementPolicy {
16 LoadAware,
17 LeastLoaded,
18 RoundRobin,
19}
20
21#[derive(Clone, Copy, Default)]
22struct NodeLoad {
23 kv_used: u64,
24 kv_total: u64,
25 lanes_active: u32,
26 queue_depth: u32,
27 decode_tokens_per_s: u32,
28 prefill_tokens_per_s: u32,
29 at: Option<std::time::Instant>,
30}
31
32impl NodeLoad {
33 const TTL: std::time::Duration = std::time::Duration::from_secs(10);
34
35 fn fresh(&self) -> bool {
36 self.at.is_some_and(|t| t.elapsed() < Self::TTL)
37 }
38
39 fn pool_pct(&self) -> Option<u64> {
40 (self.fresh() && self.kv_total > 0).then(|| self.kv_used * 100 / self.kv_total)
41 }
42
43 fn eta_ms(&self, inflight: u32, prompt_tokens: u64, expected_out: u64, typical: (u64, u64)) -> u64 {
46 let ahead = if self.fresh() {
47 self.lanes_active as u64 + self.queue_depth as u64 + inflight as u64
48 } else {
49 inflight as u64
50 };
51 const NOMINAL_DECODE: u64 = 20;
52 const NOMINAL_PREFILL: u64 = 200;
53 let or = |reported: u32, typical: u64, nominal: u64| match (reported, typical) {
56 (0, 0) => nominal,
57 (0, t) => t,
58 (r, _) => r as u64,
59 };
60 let decode = or(self.decode_tokens_per_s, typical.0, NOMINAL_DECODE);
61 let prefill = or(self.prefill_tokens_per_s, typical.1, NOMINAL_PREFILL);
62 let queued = ahead.saturating_mul(expected_out).saturating_mul(1_000) / decode;
63 let mine = prompt_tokens.saturating_mul(1_000) / prefill
64 + expected_out.saturating_mul(1_000) / decode;
65 queued.saturating_add(mine)
66 }
67}
68
69fn watch(node: &Arc<NodeConn>, head: &mut FleetHead) {
71 let node = Arc::downgrade(node);
72 head.on_status(Arc::new(move |cap| {
73 if let Some(node) = node.upgrade() {
74 *node.live.lock().expect("live") = FleetManager::load_from(cap);
75 }
76 }));
77}
78
79#[derive(Debug, Clone)]
80pub struct NodeSpec {
81 pub addr: String,
82 pub auth: Vec<u8>,
83}
84
85struct NodeConn {
86 spec: NodeSpec,
87 conns: Vec<Mutex<Option<FleetHead>>>,
88 control: Mutex<Option<FleetHead>>,
92 open: std::sync::atomic::AtomicUsize,
94 cursor: std::sync::atomic::AtomicUsize,
95 decoded: std::sync::atomic::AtomicU64,
97 live: Mutex<NodeLoad>,
100}
101
102impl NodeMeta {
103 fn new() -> NodeMeta {
104 NodeMeta {
105 models: Vec::new(),
106 identity: String::new(),
107 peer: None,
108 down_since: None,
109 last_retry: None,
110 learned: false,
111 max_context_tokens: 0,
112 load: NodeLoad::default(),
113 tokenizer_refused: Vec::new(),
114 }
115 }
116
117 fn up(&self) -> bool {
118 self.down_since.is_none()
119 }
120}
121
122impl NodeConn {
123 fn checkout_next(&self) -> (usize, std::sync::MutexGuard<'_, Option<FleetHead>>) {
124 let open = self.open.load(std::sync::atomic::Ordering::Relaxed).clamp(1, self.conns.len());
125 let i = self.cursor.fetch_add(1, std::sync::atomic::Ordering::Relaxed) % open;
126 (i, self.conns[i].lock().expect("node conn"))
127 }
128
129 fn checkout_free(&self, deadline: Option<std::time::Instant>) -> Option<(usize, std::sync::MutexGuard<'_, Option<FleetHead>>)> {
134 let open = self.open.load(std::sync::atomic::Ordering::Relaxed).clamp(1, self.conns.len());
135 let start = self.cursor.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
136 let dials = !self.spec.addr.starts_with(JOINED);
137 let mut pause = std::time::Duration::from_millis(1);
138 loop {
139 let mut busy = false;
140 for k in 0..open {
141 let i = (start + k) % open;
142 match self.conns[i].try_lock() {
143 Ok(guard) if guard.is_some() || dials => return Some((i, guard)),
144 Ok(_) | Err(std::sync::TryLockError::Poisoned(_)) => {}
145 Err(std::sync::TryLockError::WouldBlock) => busy = true,
146 }
147 }
148 if !busy {
150 let i = start % open;
151 return Some((i, self.conns[i].lock().expect("node conn")));
152 }
153 if deadline.is_some_and(|at| std::time::Instant::now() >= at) {
154 return None;
155 }
156 std::thread::sleep(pause);
157 pause = (pause * 2).min(std::time::Duration::from_millis(10));
158 }
159 }
160
161 fn checkout_idle(&self) -> std::sync::MutexGuard<'_, Option<FleetHead>> {
165 let open = self.open.load(std::sync::atomic::Ordering::Relaxed).clamp(1, self.conns.len());
166 for slot in &self.conns[..open] {
167 if let Ok(guard) = slot.try_lock() {
168 return guard;
169 }
170 }
171 let control = self.control.lock().expect("node conn");
172 if control.is_some() || !self.spec.addr.starts_with(JOINED) {
173 return control;
174 }
175 drop(control);
176 self.checkout_next().1
177 }
178
179 fn checkout_at(&self, i: usize) -> std::sync::MutexGuard<'_, Option<FleetHead>> {
180 self.conns[i.min(self.conns.len() - 1)].lock().expect("node conn")
181 }
182
183 fn checkout_at_by(
185 &self,
186 i: usize,
187 deadline: Option<std::time::Instant>,
188 ) -> Option<std::sync::MutexGuard<'_, Option<FleetHead>>> {
189 let Some(at) = deadline else { return Some(self.checkout_at(i)) };
190 let slot = &self.conns[i.min(self.conns.len() - 1)];
191 let mut pause = std::time::Duration::from_millis(1);
192 loop {
193 match slot.try_lock() {
194 Ok(guard) => return Some(guard),
195 Err(std::sync::TryLockError::Poisoned(_)) => return Some(self.checkout_at(i)),
196 Err(std::sync::TryLockError::WouldBlock) => {}
197 }
198 let left = at.saturating_duration_since(std::time::Instant::now());
199 if left.is_zero() {
200 return None;
201 }
202 std::thread::sleep(pause.min(left));
203 pause = (pause * 2).min(std::time::Duration::from_millis(10));
204 }
205 }
206
207 fn reset(&self) {
208 for slot in self.conns.iter().chain([&self.control]) {
209 if let Ok(mut g) = slot.try_lock() {
210 *g = None;
211 }
212 }
213 }
214}
215
216#[derive(Clone)]
217struct NodeMeta {
218 models: Vec<String>,
219 identity: String,
220 peer: Option<std::net::IpAddr>,
222 down_since: Option<std::time::Instant>,
224 last_retry: Option<std::time::Instant>,
225 learned: bool,
226 max_context_tokens: u64,
227 load: NodeLoad,
228 tokenizer_refused: Vec<String>,
230}
231
232#[derive(Debug, Clone)]
233struct Placed {
234 node: usize,
235 params: GenParams,
236 qos_class: u8,
237 codec: String,
238 model: String,
239 tokens: Vec<u32>,
240 slot: usize,
241 epoch: u64,
242 parked: bool,
243}
244
245struct Shared {
246 meta: Vec<NodeMeta>,
247 forgets: Vec<(usize, usize, u64, u64)>,
249 sessions: BTreeMap<u64, Placed>,
250 rr_cursor: usize,
251 cancels: BTreeMap<u64, Arc<AtomicBool>>,
252 inflight: Vec<u32>,
253 claims: BTreeMap<u64, usize>,
254}
255
256pub struct FleetManager {
257 nodes: std::sync::RwLock<Vec<Arc<NodeConn>>>,
258 conns_by_lanes: bool,
259 retry_every: std::time::Duration,
260 liveness: Liveness,
261 tokenizers: std::sync::RwLock<std::collections::HashMap<String, u64>>,
264 shared: Mutex<Shared>,
265 warm: Mutex<crate::fleet_warm::Warm>,
267 policy: PlacementPolicy,
268 lease: Lease,
269 pool_high_pct: u32,
270}
271
272struct InFlight<'a> {
273 shared: &'a Mutex<Shared>,
274 node: usize,
275 session: u64,
276}
277
278impl<'a> InFlight<'a> {
279 fn claim(shared: &'a Mutex<Shared>, node: usize, session: u64) {
280 let mut sh = shared.lock().expect("shared");
281 if let Some(prev) = sh.claims.insert(session, node) {
282 if prev != node {
283 if let Some(n) = sh.inflight.get_mut(prev) {
284 *n = n.saturating_sub(1);
285 }
286 } else {
287 return;
288 }
289 }
290 if let Some(n) = sh.inflight.get_mut(node) {
291 *n = n.saturating_add(1);
292 }
293 }
294
295 fn release(shared: &Mutex<Shared>, session: u64) {
296 let mut sh = shared.lock().expect("shared");
297 if let Some(node) = sh.claims.remove(&session) {
298 if let Some(n) = sh.inflight.get_mut(node) {
299 *n = n.saturating_sub(1);
300 }
301 }
302 }
303
304 fn guard(shared: &'a Mutex<Shared>, node: usize, session: u64) -> InFlight<'a> {
305 Self::claim(shared, node, session);
306 InFlight { shared, node, session }
307 }
308}
309
310impl Drop for InFlight<'_> {
311 fn drop(&mut self) {
312 let _ = self.node;
313 Self::release(self.shared, self.session);
314 }
315}
316
317#[derive(Clone, Copy, Default)]
318pub struct RequestShape {
319 pub prompt_tokens: u64,
320 pub expected_out: u64,
321 pub warm: Option<usize>,
324}
325
326pub const DEFAULT_POOL_HIGH_PCT: u32 = 90;
327
328pub const DEFAULT_CONNS_PER_NODE: usize = 0;
330
331const MAX_CONNS_PER_NODE: usize = 64;
332
333const JOINED: &str = "joined:";
335
336pub const RETRY_DOWN_AFTER: std::time::Duration = std::time::Duration::from_secs(10);
338
339#[derive(Debug, Clone)]
341pub struct NodeStatus {
342 pub name: String,
344 pub addr: String,
346 pub up: bool,
347 pub lanes: usize,
349 pub lanes_active: u32,
351 pub queue_depth: u32,
352 pub kv_blocks_used: u64,
353 pub kv_blocks_total: u64,
354 pub decode_tokens: u64,
355}
356
357#[derive(Debug, Clone)]
358pub struct NodeReport {
359 pub addr: String,
360 pub identity: String,
361 pub models: Vec<String>,
362 pub max_context_tokens: u64,
363 pub max_lanes: u32,
364}
365
366impl FleetManager {
367 pub fn new(nodes: Vec<NodeSpec>, policy: PlacementPolicy, lease: Lease) -> FleetManager {
368 Self::with_pool_high(nodes, policy, lease, DEFAULT_POOL_HIGH_PCT)
369 }
370
371 pub fn with_pool_high(
372 nodes: Vec<NodeSpec>,
373 policy: PlacementPolicy,
374 lease: Lease,
375 pool_high_pct: u32,
376 ) -> FleetManager {
377 Self::with_options(nodes, policy, lease, pool_high_pct, DEFAULT_CONNS_PER_NODE)
378 }
379
380 pub fn with_options(
381 nodes: Vec<NodeSpec>,
382 policy: PlacementPolicy,
383 lease: Lease,
384 pool_high_pct: u32,
385 conns_per_node: usize,
386 ) -> FleetManager {
387 let meta = vec![NodeMeta::new(); nodes.len()];
388 let node_count = nodes.len();
389 let nodes = nodes
390 .into_iter()
391 .map(|spec| {
392 Arc::new(NodeConn {
393 spec,
394 conns: (0..if conns_per_node == 0 { MAX_CONNS_PER_NODE } else { conns_per_node })
395 .map(|_| Mutex::new(None))
396 .collect(),
397 control: Mutex::new(None),
398 open: std::sync::atomic::AtomicUsize::new(conns_per_node.max(1)),
399 cursor: std::sync::atomic::AtomicUsize::new(0),
400 decoded: std::sync::atomic::AtomicU64::new(0),
401 live: Mutex::new(NodeLoad::default()),
402 })
403 })
404 .collect();
405 FleetManager {
406 nodes: std::sync::RwLock::new(nodes),
407 conns_by_lanes: conns_per_node == 0,
408 retry_every: RETRY_DOWN_AFTER,
409 liveness: Liveness::DEFAULT,
410 tokenizers: Default::default(),
411 warm: Default::default(),
412 shared: Mutex::new(Shared {
413 meta,
414 sessions: BTreeMap::new(),
415 forgets: Vec::new(),
416 rr_cursor: 0,
417 inflight: vec![0; node_count],
418 claims: BTreeMap::new(),
419 cancels: BTreeMap::new(),
420 }),
421 policy,
422 lease,
423 pool_high_pct: pool_high_pct.clamp(1, 100),
424 }
425 }
426
427 fn node(&self, i: usize) -> Arc<NodeConn> {
428 Arc::clone(&self.nodes.read().expect("nodes")[i])
429 }
430
431 fn node_count(&self) -> usize {
432 self.nodes.read().expect("nodes").len()
433 }
434
435 pub fn adopt(&self, head: FleetHead, peer: std::net::IpAddr) -> Result<(usize, bool, usize), String> {
443 let identity = head.node().node_identity.clone();
444 let lanes = (head.node().capacity.max_lanes as usize).max(1);
445 let fresh: Arc<NodeConn>;
449 let mut first = None;
450 let (i, new) = {
451 let mut sh = self.shared.lock().expect("shared");
452 match sh.meta.iter().position(|m| m.identity == identity) {
453 Some(i) => {
454 let node = self.node(i);
455 let connected = node.conns.iter().any(in_use);
456 if connected && sh.meta[i].peer.is_some_and(|p| p != peer) {
457 return Err(format!(
458 "another node named '{identity}' is joined from {}; give this one --identity",
459 sh.meta[i].peer.expect("peer")
460 ));
461 }
462 sh.meta[i].peer = Some(peer);
463 (i, false)
464 }
465 None => {
466 fresh = Arc::new(NodeConn {
467 spec: NodeSpec { addr: format!("{JOINED}{identity}"), auth: Vec::new() },
468 conns: (0..lanes).map(|_| Mutex::new(None)).collect(),
469 control: Mutex::new(None),
470 open: std::sync::atomic::AtomicUsize::new(1),
471 cursor: std::sync::atomic::AtomicUsize::new(0),
472 decoded: std::sync::atomic::AtomicU64::new(0),
473 live: Mutex::new(NodeLoad::default()),
474 });
475 first = Some(fresh.conns[0].lock().expect("node conn"));
476 let mut nodes = self.nodes.write().expect("nodes");
477 nodes.push(Arc::clone(&fresh));
478 let mut meta = NodeMeta::new();
481 meta.identity = identity.clone();
482 meta.peer = Some(peer);
483 meta.down_since = Some(std::time::Instant::now());
484 sh.meta.push(meta);
485 sh.inflight.push(0);
486 (nodes.len() - 1, true)
487 }
488 }
489 };
490 self.sync_meta(i, &head);
491 let node = self.node(i);
492 let mut head = head;
493 head.keep_alive(self.liveness);
494 watch(&node, &mut head);
495 if let Some(mut first) = first {
496 let _ = head.ask_control();
498 *first = Some(head);
499 } else {
500 let slot = node.conns.iter().position(|c| !in_use(c));
501 let has_control = node.control.lock().expect("node conn").is_some();
502 match slot {
503 Some(slot) => {
504 if !has_control {
505 let _ = head.ask_control();
506 }
507 *node.checkout_at(slot) = Some(head);
508 }
509 None if !has_control => *node.control.lock().expect("node conn") = Some(head),
511 None => *node.checkout_at(0) = Some(head),
512 }
513 }
514 let open = node.conns.iter().filter(|c| in_use(c)).count();
515 node.open.store(open.max(1), std::sync::atomic::Ordering::Relaxed);
516 if new {
517 self.shared.lock().expect("shared").meta[i].down_since = None;
519 } else {
520 self.mark_up(i);
521 }
522 Ok((i, new, open))
523 }
524
525 pub fn connections(&self, i: usize) -> usize {
527 self.node(i).open.load(std::sync::atomic::Ordering::Relaxed)
528 }
529
530 pub fn load(&self) -> Vec<usize> {
531 let sh = self.shared.lock().expect("shared");
532 let mut counts = vec![0usize; self.node_count()];
533 for p in sh.sessions.values() {
534 if !p.parked {
535 counts[p.node] += 1;
536 }
537 }
538 counts
539 }
540
541 pub fn status(&self) -> Vec<NodeStatus> {
543 let nodes: Vec<Arc<NodeConn>> = self.nodes.read().expect("nodes").clone();
544 let sh = self.shared.lock().expect("shared");
545 nodes
546 .iter()
547 .zip(&sh.meta)
548 .zip(&sh.inflight)
549 .map(|((node, meta), &inflight)| {
550 let live = *node.live.lock().expect("live");
551 let load = if live.at > meta.load.at { live } else { meta.load };
552 let addr = match (&meta.peer, node.spec.addr.strip_prefix(JOINED)) {
553 (Some(peer), Some(_)) => peer.to_string(),
554 _ => node.spec.addr.clone(),
555 };
556 NodeStatus {
557 name: if meta.identity.is_empty() { addr.clone() } else { meta.identity.clone() },
558 addr,
559 up: meta.up() && meta.learned,
560 lanes: node.open.load(Ordering::Relaxed),
561 lanes_active: inflight,
562 queue_depth: load.queue_depth,
563 kv_blocks_used: load.kv_used,
564 kv_blocks_total: load.kv_total,
565 decode_tokens: node.decoded.load(Ordering::Relaxed),
566 }
567 })
568 .collect()
569 }
570
571 pub fn nodes_up(&self) -> (usize, usize) {
574 let sh = self.shared.lock().expect("shared");
575 (sh.meta.iter().filter(|m| m.up() && m.learned).count(), sh.meta.len())
576 }
577
578 pub fn serving(&self, model: &str) -> (usize, usize) {
579 let sh = self.shared.lock().expect("shared");
580 let up = (0..sh.meta.len())
581 .filter(|&i| sh.meta[i].up() && sh.meta[i].learned && Self::serves(&sh, i, model))
582 .count();
583 (up, sh.meta.len())
584 }
585
586 pub fn node_of(&self, session: u64) -> Option<String> {
587 let sh = self.shared.lock().expect("shared");
588 let node = sh.sessions.get(&session)?.node;
589 Some(sh.meta[node].identity.clone())
590 }
591
592 pub fn with_retry_every(mut self, every: std::time::Duration) -> FleetManager {
595 self.retry_every = every;
596 self
597 }
598
599 pub fn expect_tokenizer(&self, model: &str, digest: u64) {
602 self.tokenizers.write().expect("tokenizers").insert(model.to_string(), digest);
603 }
604
605 pub fn with_liveness(mut self, liveness: Liveness) -> FleetManager {
608 self.liveness = liveness;
609 self
610 }
611
612 pub fn with_warm_log(mut self, path: std::path::PathBuf) -> FleetManager {
615 self.warm = Mutex::new(crate::fleet_warm::Warm::kept_in(path));
616 self
617 }
618
619 fn mark_unhealthy(&self, i: usize, why: &dyn std::fmt::Display) {
620 let mut sh = self.shared.lock().expect("shared");
621 if sh.meta[i].down_since.is_none() {
622 let now = std::time::Instant::now();
623 sh.meta[i].down_since = Some(now);
624 sh.meta[i].last_retry = Some(now);
625 tracing::warn!("fleet: node {} is down ({why}); retrying it in the background", self.label(&sh, i));
626 }
627 }
628
629 fn mark_up(&self, i: usize) {
630 let mut sh = self.shared.lock().expect("shared");
631 if let Some(since) = sh.meta[i].down_since.take() {
632 tracing::info!("fleet: node {} is back after {} s", self.label(&sh, i), since.elapsed().as_secs());
633 }
634 }
635
636 fn label(&self, sh: &Shared, i: usize) -> String {
637 let node = self.node(i);
638 let addr = &node.spec.addr;
639 match sh.meta[i].identity.as_str() {
640 "" => addr.clone(),
641 id => format!("{id} ({addr})"),
642 }
643 }
644
645 fn identity(&self, i: usize) -> String {
646 let sh = self.shared.lock().expect("shared");
647 self.label(&sh, i)
648 }
649
650 fn active_node(&self, session: u64) -> Result<usize, DaemonError> {
651 let sh = self.shared.lock().expect("shared");
652 let p = sh
653 .sessions
654 .get(&session)
655 .ok_or(DaemonError::UnknownSession(session))?;
656 if p.parked {
657 return Err(DaemonError::Config("session is parked; resume it first"));
658 }
659 Ok(p.node)
660 }
661
662 fn ensure_connected(&self, i: usize, guard: &mut Option<FleetHead>) -> Result<(), DaemonError> {
663 if guard.is_some() {
664 return Ok(());
665 }
666 let node = self.node(i);
667 let spec = &node.spec;
668 if spec.addr.starts_with(JOINED) {
669 let e = DaemonError::FleetNode { addr: spec.addr.clone(), why: "left; waiting for it to join again".into() };
670 self.mark_unhealthy(i, &e);
671 return Err(e);
672 }
673 match FleetHead::connect(&spec.addr, spec.auth.clone()) {
674 Ok(mut head) => {
675 head.keep_alive(self.liveness);
676 watch(&node, &mut head);
677 self.sync_meta(i, &head);
678 *guard = Some(head);
679 Ok(())
680 }
681 Err(e) => {
682 self.mark_unhealthy(i, &e);
683 Err(e)
684 }
685 }
686 }
687
688 fn rebind(&self, session: u64, head: &mut FleetHead) -> Result<(), DaemonError> {
691 if head.knows(session) {
692 return Ok(());
693 }
694 let p = self
695 .shared
696 .lock()
697 .expect("shared")
698 .sessions
699 .get(&session)
700 .cloned()
701 .ok_or(DaemonError::UnknownSession(session))?;
702 let epoch = head.assign(session, p.params, p.qos_class, &p.codec, self.lease)?;
703 if !p.tokens.is_empty() {
704 head.append(session, &p.tokens)?;
705 }
706 if let Some(p) = self.shared.lock().expect("shared").sessions.get_mut(&session) {
707 p.epoch = epoch;
708 }
709 Ok(())
710 }
711
712 fn rebind_on(&self, session: u64, guard: &mut Option<FleetHead>) -> Result<(), DaemonError> {
715 let r = self.rebind(session, guard.as_mut().expect("connected"));
716 if let Err(e) = &r {
717 if is_fatal_node(e) {
718 *guard = None;
719 }
720 }
721 r
722 }
723
724 pub fn advertised_context(&self, model: &str) -> Option<u64> {
725 let sh = self.shared.lock().expect("shared");
726 (0..self.node_count())
727 .filter(|&i| sh.meta[i].up() && Self::serves(&sh, i, model))
728 .map(|i| sh.meta[i].max_context_tokens)
729 .filter(|&c| c > 0)
730 .max()
731 }
732
733 fn serves(sh: &Shared, i: usize, model: &str) -> bool {
734 !sh.meta[i].learned || sh.meta[i].models.iter().any(|m| m == model)
735 }
736
737 fn holds(sh: &Shared, i: usize, len: u64) -> bool {
738 let max = sh.meta[i].max_context_tokens;
739 max == 0 || len <= max
740 }
741
742 fn length_refusal(&self, cand: &[usize], model: &str, len: u64) -> Option<DaemonError> {
743 let sh = self.shared.lock().expect("shared");
744 let eligible: Vec<usize> =
745 cand.iter().copied().filter(|&i| Self::serves(&sh, i, model)).collect();
746 if eligible.is_empty() || eligible.iter().any(|&i| Self::holds(&sh, i, len)) {
747 return None;
748 }
749 Some(DaemonError::StreamTooLong {
750 len,
751 max: eligible.iter().map(|&i| sh.meta[i].max_context_tokens).max().unwrap_or(0),
752 })
753 }
754
755 fn sync_meta(&self, i: usize, head: &FleetHead) {
756 let mut sh = self.shared.lock().expect("shared");
757 let cap = &head.node().capacity;
758 let theirs = head.tokenizer();
759 let (models, refused): (Vec<String>, Vec<String>) = {
760 let ours = self.tokenizers.read().expect("tokenizers");
761 head.node().loadable_models.iter().cloned().partition(|m| {
762 ours.get(m).is_none_or(|&d| d == 0 || theirs == 0 || d == theirs)
763 })
764 };
765 if !refused.is_empty() && sh.meta[i].tokenizer_refused != refused {
766 tracing::warn!(
767 "fleet: node {} tokenizes {} otherwise than this head does, so nothing for it is placed there; \
768 serve the same build of the model on both",
769 self.label(&sh, i),
770 refused.join(", ")
771 );
772 }
773 sh.meta[i].tokenizer_refused = refused;
774 sh.meta[i].models = models;
775 sh.meta[i].max_context_tokens = cap.max_context_tokens;
776 sh.meta[i].identity = head.node().node_identity.clone();
777 sh.meta[i].learned = true;
778 sh.meta[i].load = Self::load_from(cap);
779 let node = self.node(i);
780 if self.conns_by_lanes && !node.spec.addr.starts_with(JOINED) {
781 let (lanes, pool) = (cap.max_lanes.max(1) as usize, node.conns.len());
782 let was = node.open.swap(lanes.min(pool), std::sync::atomic::Ordering::Relaxed);
783 if lanes > pool && was != pool {
784 tracing::warn!(
785 "fleet: node {} has {lanes} lanes; the head opens at most {pool} connections to it",
786 self.label(&sh, i)
787 );
788 }
789 }
790 }
791
792 fn load_from(cap: &superfluid_proto::handshake::CapacitySummary) -> NodeLoad {
793 NodeLoad {
794 kv_used: cap.kv_blocks_used,
795 kv_total: cap.kv_blocks_total,
796 lanes_active: cap.lanes_active,
797 queue_depth: cap.queue_depth,
798 decode_tokens_per_s: cap.decode_tokens_per_s,
799 prefill_tokens_per_s: cap.prefill_tokens_per_s,
800 at: Some(std::time::Instant::now()),
801 }
802 }
803
804 fn replacement_shape(tokens: &[u32], expected_out: u64) -> RequestShape {
805 RequestShape {
806 prompt_tokens: tokens.len() as u64,
807 expected_out,
808 warm: None,
809 }
810 }
811
812 fn candidates_claiming(
813 &self,
814 model: &str,
815 claim_for: Option<u64>,
816 shape: RequestShape,
817 ) -> Vec<usize> {
818 let mut sh = self.shared.lock().expect("shared");
819 let mut load = vec![0usize; self.node_count()];
820 for p in sh.sessions.values() {
821 if !p.parked {
822 load[p.node] += 1;
823 }
824 }
825 let mut cand: Vec<usize> = (0..self.node_count())
826 .filter(|&i| sh.meta[i].up() && Self::serves(&sh, i, model))
827 .collect();
828 match self.policy {
829 PlacementPolicy::LoadAware => {
830 let fits: Vec<usize> = cand
831 .iter()
832 .copied()
833 .filter(|&i| Self::holds(&sh, i, shape.prompt_tokens))
834 .collect();
835 let judged: &[usize] = if fits.is_empty() { &cand } else { &fits };
836 let hot: Vec<usize> = judged
837 .iter()
838 .copied()
839 .filter(|&i| {
840 sh.meta[i].load.pool_pct().is_some_and(|p| p >= self.pool_high_pct as u64)
841 })
842 .collect();
843 if hot.len() < judged.len() {
844 cand.retain(|i| !hot.contains(i));
845 }
846 let inflight = sh.inflight.clone();
847 let mean = |rate: fn(&NodeLoad) -> u32| {
848 let known: Vec<u64> =
849 cand.iter().map(|&i| rate(&sh.meta[i].load) as u64).filter(|&r| r > 0).collect();
850 known.iter().sum::<u64>() / (known.len() as u64).max(1)
851 };
852 let typical = (mean(|l| l.decode_tokens_per_s), mean(|l| l.prefill_tokens_per_s));
853 cand.sort_by_key(|&i| {
854 (
855 sh.meta[i].load.eta_ms(inflight[i], shape.prompt_tokens, shape.expected_out, typical),
856 load[i],
857 )
858 });
859 }
860 PlacementPolicy::LeastLoaded => {
861 let inflight = sh.inflight.clone();
862 cand.sort_by_key(|&i| load[i] + inflight[i] as usize);
863 }
864 PlacementPolicy::RoundRobin => {
865 let n = self.node_count().max(1);
866 let c = sh.rr_cursor % n;
867 cand.sort_by_key(|&i| (i + n - c) % n);
868 sh.rr_cursor = sh.rr_cursor.wrapping_add(1);
869 }
870 }
871 if let Some(w) = shape.warm.filter(|_| self.policy != PlacementPolicy::RoundRobin) {
872 if let Some(at) = cand.iter().position(|&i| i == w) {
873 let lanes = self.node(w).open.load(Ordering::Relaxed).max(1);
874 if (sh.inflight[w] as usize) < lanes {
875 let w = cand.remove(at);
876 cand.insert(0, w);
877 }
878 }
879 }
880 if let (Some(session), Some(&winner)) = (claim_for, cand.first()) {
881 Self::claim_locked(&mut sh, winner, session);
882 }
883 cand
884 }
885
886 fn checkout_live(node_conn: &NodeConn, slot: usize) -> std::sync::MutexGuard<'_, Option<FleetHead>> {
889 Self::checkout_live_by(node_conn, slot, None).expect("no deadline to pass")
890 }
891
892 fn checkout_live_by(
894 node_conn: &NodeConn,
895 slot: usize,
896 deadline: Option<std::time::Instant>,
897 ) -> Option<std::sync::MutexGuard<'_, Option<FleetHead>>> {
898 let mut guard = node_conn.checkout_at_by(slot, deadline)?;
899 if guard.as_mut().is_some_and(|h| h.drain_status().is_err()) {
900 *guard = None;
901 }
902 Some(guard)
903 }
904
905 fn slot_of(&self, session: u64) -> usize {
906 self.shared
907 .lock()
908 .expect("shared")
909 .sessions
910 .get(&session)
911 .map(|p| p.slot)
912 .unwrap_or(0)
913 }
914
915 fn claim_locked(sh: &mut Shared, node: usize, session: u64) {
916 if let Some(prev) = sh.claims.insert(session, node) {
917 if prev == node {
918 return;
919 }
920 if let Some(n) = sh.inflight.get_mut(prev) {
921 *n = n.saturating_sub(1);
922 }
923 }
924 if let Some(n) = sh.inflight.get_mut(node) {
925 *n = n.saturating_add(1);
926 }
927 }
928
929 #[allow(clippy::too_many_arguments)]
930 pub fn place(
931 &self,
932 session: u64,
933 params: GenParams,
934 qos_class: u8,
935 codec: &str,
936 model: &str,
937 tokens: &[u32],
938 expected_out: u64,
939 ) -> Result<usize, DaemonError> {
940 let placed = self.place_by(session, params, qos_class, codec, model, tokens, expected_out, None)?;
941 Ok(placed.expect("no deadline to pass"))
942 }
943
944 #[allow(clippy::too_many_arguments)]
948 pub fn place_by(
949 &self,
950 session: u64,
951 params: GenParams,
952 qos_class: u8,
953 codec: &str,
954 model: &str,
955 tokens: &[u32],
956 expected_out: u64,
957 deadline: Option<std::time::Instant>,
958 ) -> Result<Option<usize>, DaemonError> {
959 let warm = self.warm_node(model, tokens);
960 let cand = self.candidates_claiming(
961 model,
962 Some(session),
963 RequestShape {
964 prompt_tokens: tokens.len() as u64,
965 expected_out,
966 warm,
967 },
968 );
969 let release_on_error = |e: DaemonError| {
970 InFlight::release(&self.shared, session);
971 e
972 };
973 if cand.is_empty() {
974 return Err(release_on_error(DaemonError::NoNode(
975 "no healthy node can serve the model",
976 )));
977 }
978 let len = tokens.len() as u64;
979 if let Some(e) = self.length_refusal(&cand, model, len) {
980 return Err(release_on_error(e));
981 }
982 let late = || deadline.is_some_and(|at| std::time::Instant::now() >= at);
983 let mut last_err = None;
984 for &i in &cand {
985 if late() {
986 break;
987 }
988 match self.try_place_by(i, session, params, qos_class, codec, model, tokens, deadline) {
989 Ok(true) => {
990 tracing::info!(
991 "fleet: session {session} ({} prompt tokens) placed on {}",
992 tokens.len(),
993 self.identity(i)
994 );
995 return Ok(Some(i));
996 }
997 Ok(false) => continue,
998 Err(e) => {
999 self.mark_unhealthy(i, &e);
1000 self.node(i).reset();
1001 last_err = Some(e);
1002 }
1003 }
1004 }
1005 if late() {
1006 InFlight::release(&self.shared, session);
1007 return Ok(None);
1008 }
1009 if let Some(e) = self.length_refusal(&cand, model, len) {
1010 return Err(release_on_error(e));
1011 }
1012 Err(release_on_error(last_err.unwrap_or(DaemonError::NoNode(
1013 "no reachable node serves the model",
1014 ))))
1015 }
1016
1017 #[allow(clippy::too_many_arguments)]
1018 fn try_place_on(
1019 &self,
1020 i: usize,
1021 session: u64,
1022 params: GenParams,
1023 qos_class: u8,
1024 codec: &str,
1025 model: &str,
1026 tokens: &[u32],
1027 ) -> Result<bool, DaemonError> {
1028 self.try_place_by(i, session, params, qos_class, codec, model, tokens, None)
1029 }
1030
1031 #[allow(clippy::too_many_arguments)]
1034 fn try_place_by(
1035 &self,
1036 i: usize,
1037 session: u64,
1038 params: GenParams,
1039 qos_class: u8,
1040 codec: &str,
1041 model: &str,
1042 tokens: &[u32],
1043 deadline: Option<std::time::Instant>,
1044 ) -> Result<bool, DaemonError> {
1045 let node = self.node(i);
1046 let Some((slot, mut guard)) = node.checkout_free(deadline) else {
1047 return Ok(false);
1048 };
1049 if let Some(h) = guard.as_mut() {
1050 if h.drain_status().is_err() {
1051 *guard = None;
1052 }
1053 }
1054 self.ensure_connected(i, &mut guard)?;
1055 self.flush_forgets(i, slot, guard.as_mut().expect("connected"));
1056 self.sync_meta(i, guard.as_ref().expect("connected"));
1057 {
1058 let sh = self.shared.lock().expect("shared");
1059 if !Self::serves(&sh, i, model) {
1060 return Ok(false);
1061 }
1062 if !Self::holds(&sh, i, tokens.len() as u64) {
1063 return Ok(false);
1064 }
1065 }
1066 let head = guard.as_mut().expect("connected");
1067 let epoch = match head.assign(session, params, qos_class, codec, self.lease) {
1068 Ok(epoch) => epoch,
1069 Err(e) => {
1070 *guard = None;
1071 return Err(e);
1072 }
1073 };
1074 if !tokens.is_empty() {
1075 if let Err(e) = head.append(session, tokens) {
1076 *guard = None;
1077 return Err(e);
1078 }
1079 }
1080 InFlight::claim(&self.shared, i, session);
1081 self.shared.lock().expect("shared").sessions.insert(
1082 session,
1083 Placed {
1084 node: i,
1085 params,
1086 qos_class,
1087 codec: codec.to_string(),
1088 model: model.to_string(),
1089 tokens: tokens.to_vec(),
1090 slot,
1091 epoch,
1092 parked: false,
1093 },
1094 );
1095 Ok(true)
1096 }
1097
1098 pub fn append(&self, session: u64, tokens: &[u32]) -> Result<(), DaemonError> {
1099 let node = self.active_node(session)?;
1100 let len = tokens.len() as u64;
1101 let fits = {
1102 let sh = self.shared.lock().expect("shared");
1103 Self::holds(&sh, node, len)
1104 };
1105 if !fits {
1106 return self.re_place_grown(session, node, tokens);
1107 }
1108 {
1109 let node_conn = self.node(node);
1110 let mut guard = Self::checkout_live(&node_conn, self.slot_of(session));
1111 self.ensure_connected(node, &mut guard)?;
1112 self.rebind_on(session, &mut guard)?;
1113 let head = guard.as_mut().expect("connected");
1114 if let Err(e) = head.append(session, tokens) {
1115 *guard = None;
1116 return Err(e);
1117 }
1118 self.sync_meta(node, head);
1119 }
1120 if let Some(p) = self.shared.lock().expect("shared").sessions.get_mut(&session) {
1121 p.tokens = tokens.to_vec();
1122 }
1123 Ok(())
1124 }
1125
1126 fn re_place_grown(
1127 &self,
1128 session: u64,
1129 from: usize,
1130 tokens: &[u32],
1131 ) -> Result<(), DaemonError> {
1132 let placed = {
1133 let sh = self.shared.lock().expect("shared");
1134 sh.sessions.get(&session).cloned().ok_or(DaemonError::UnknownSession(session))?
1135 };
1136 let cand: Vec<usize> = self
1137 .candidates_claiming(
1138 &placed.model,
1139 Some(session),
1140 Self::replacement_shape(tokens, 0),
1141 )
1142 .into_iter()
1143 .filter(|&i| i != from)
1144 .collect();
1145 let len = tokens.len() as u64;
1146 if let Some(e) = self.length_refusal(&cand, &placed.model, len) {
1147 return Err(e);
1148 }
1149 for &i in &cand {
1150 match self.try_place_on(
1151 i,
1152 session,
1153 placed.params,
1154 placed.qos_class,
1155 &placed.codec,
1156 &placed.model,
1157 tokens,
1158 ) {
1159 Ok(true) => return Ok(()),
1160 Ok(false) => continue,
1161 Err(e) => {
1162 self.mark_unhealthy(i, &e);
1163 self.node(i).reset();
1164 }
1165 }
1166 }
1167 if let Some(e) = self.length_refusal(&cand, &placed.model, len) {
1168 return Err(e);
1169 }
1170 Err(DaemonError::Config(
1171 "no reachable node can hold the session's grown context",
1172 ))
1173 }
1174
1175 pub fn generate(
1176 &self,
1177 session: u64,
1178 max_new_tokens: u64,
1179 max_wall_ms: u64,
1180 ) -> Result<FleetGeneration, DaemonError> {
1181 let node = self.active_node(session)?;
1182
1183 match self.generate_on(node, session, max_new_tokens, max_wall_ms) {
1184 Ok(g) => return Ok(g),
1185 Err(e) if !is_fatal_node(&e) => return Err(e),
1186 Err(e) => self.mark_unhealthy(node, &e),
1187 }
1188 self.node(node).reset();
1189 let placed = self
1190 .shared
1191 .lock()
1192 .expect("shared")
1193 .sessions
1194 .get(&session)
1195 .cloned()
1196 .expect("placed session");
1197 let cand: Vec<usize> = self
1198 .candidates_claiming(
1199 &placed.model,
1200 Some(session),
1201 Self::replacement_shape(&placed.tokens, max_new_tokens),
1202 )
1203 .into_iter()
1204 .filter(|&i| i != node)
1205 .collect();
1206 for i in cand {
1207 match self.try_place_on(
1208 i,
1209 session,
1210 placed.params,
1211 placed.qos_class,
1212 &placed.codec,
1213 &placed.model,
1214 &placed.tokens,
1215 ) {
1216 Ok(true) => {}
1217 Ok(false) => continue,
1218 Err(e) => {
1219 self.mark_unhealthy(i, &e);
1220 self.node(i).reset();
1221 continue;
1222 }
1223 }
1224 tracing::warn!("fleet: session {session} moved to {}", self.identity(i));
1225 match self.generate_on(i, session, max_new_tokens, max_wall_ms) {
1226 Ok(g) => return Ok(g),
1227 Err(e) => self.mark_unhealthy(i, &e),
1228 }
1229 self.node(i).reset();
1230 }
1231 Err(DaemonError::Generation(
1232 "generation failed and no healthy node could take over the session".into(),
1233 ))
1234 }
1235
1236 fn map_refusal(
1237 &self,
1238 node: usize,
1239 session: u64,
1240 r: Result<FleetGeneration, DaemonError>,
1241 ) -> Result<FleetGeneration, DaemonError> {
1242 let g = r?;
1243 if g.finish == crate::nodeagent::FINISH_DECLINED {
1244 return Err(DaemonError::Generation(format!(
1245 "node {} declined session {session} (stale epoch or replayed command)",
1246 self.shared.lock().expect("shared").meta[node].identity
1247 )));
1248 }
1249 if g.finish != crate::nodeagent::FINISH_REFUSED {
1250 return Ok(g);
1251 }
1252 let sh = self.shared.lock().expect("shared");
1253 let len = sh.sessions.get(&session).map(|p| p.tokens.len() as u64).unwrap_or(0);
1254 Err(DaemonError::StreamTooLong {
1255 len,
1256 max: sh.meta[node].max_context_tokens,
1257 })
1258 }
1259
1260 fn generate_on(
1261 &self,
1262 node: usize,
1263 session: u64,
1264 max_new_tokens: u64,
1265 max_wall_ms: u64,
1266 ) -> Result<FleetGeneration, DaemonError> {
1267 let _inflight = InFlight::guard(&self.shared, node, session);
1268 let node_conn = self.node(node);
1269 let mut guard = Self::checkout_live(&node_conn, self.slot_of(session));
1270 self.ensure_connected(node, &mut guard)?;
1271 self.rebind_on(session, &mut guard)?;
1272 let head = guard.as_mut().expect("connected");
1273 let r = head.generate(session, max_new_tokens, max_wall_ms);
1274 if let Ok(g) = &r {
1275 node_conn.decoded.fetch_add(g.tokens.len() as u64, Ordering::Relaxed);
1276 }
1277 if let Err(e) = &r {
1278 if is_fatal_node(e) {
1279 *guard = None;
1280 }
1281 }
1282 if let Some(head) = guard.as_ref() {
1283 self.sync_meta(node, head);
1284 }
1285 drop(guard);
1286 self.map_refusal(node, session, r)
1287 }
1288
1289 pub fn generate_streaming(
1290 &self,
1291 session: u64,
1292 max_new_tokens: u64,
1293 max_wall_ms: u64,
1294 on_tokens: impl FnMut(&[u32]),
1295 ) -> Result<FleetGeneration, DaemonError> {
1296 self.generate_streaming_with(session, max_new_tokens, max_wall_ms, None, &mut |_| {}, on_tokens)
1297 }
1298
1299 pub fn generate_streaming_with(
1302 &self,
1303 session: u64,
1304 max_new_tokens: u64,
1305 max_wall_ms: u64,
1306 opts: Option<&crate::fleet::Ask<'_>>,
1307 on_logprobs: &mut dyn FnMut(&[superfluid_proto::linkf::TokenLogprobMsg]),
1308 on_tokens: impl FnMut(&[u32]),
1309 ) -> Result<FleetGeneration, DaemonError> {
1310 self.generate_streaming_by(session, max_new_tokens, max_wall_ms, None, opts, on_logprobs, on_tokens)
1311 }
1312
1313 #[allow(clippy::too_many_arguments)]
1317 pub fn generate_streaming_by(
1318 &self,
1319 session: u64,
1320 max_new_tokens: u64,
1321 max_wall_ms: u64,
1322 deadline: Option<std::time::Instant>,
1323 opts: Option<&crate::fleet::Ask<'_>>,
1324 on_logprobs: &mut dyn FnMut(&[superfluid_proto::linkf::TokenLogprobMsg]),
1325 mut on_tokens: impl FnMut(&[u32]),
1326 ) -> Result<FleetGeneration, DaemonError> {
1327 let (node, flag) = {
1328 let mut sh = self.shared.lock().expect("shared");
1329 let p = sh
1330 .sessions
1331 .get(&session)
1332 .ok_or(DaemonError::UnknownSession(session))?;
1333 if p.parked {
1334 return Err(DaemonError::Config("session is parked; resume it first"));
1335 }
1336 let node = p.node;
1337 let flag = sh
1338 .cancels
1339 .entry(session)
1340 .or_insert_with(|| Arc::new(AtomicBool::new(false)))
1341 .clone();
1342 flag.store(false, Ordering::Relaxed);
1343 (node, flag)
1344 };
1345
1346 let mut all_tokens: Vec<u32> = Vec::new();
1347
1348 match self.generate_streaming_on(node, session, max_new_tokens, max_wall_ms, deadline, &flag, opts, on_logprobs, |b| {
1349 all_tokens.extend_from_slice(b);
1350 on_tokens(b);
1351 }) {
1352 Ok(g) => return Ok(g),
1353 Err(e) if !is_fatal_node(&e) => return Err(e),
1354 Err(e) => self.mark_unhealthy(node, &e),
1355 }
1356
1357 let placed = self
1358 .shared
1359 .lock()
1360 .expect("shared")
1361 .sessions
1362 .get(&session)
1363 .cloned()
1364 .expect("placed session");
1365 let input = placed.tokens.clone();
1366 let mut tried = vec![node];
1367 loop {
1368 let remaining = max_new_tokens.saturating_sub(all_tokens.len() as u64);
1369 if remaining == 0 {
1370 return Ok(FleetGeneration {
1371 tokens: all_tokens,
1372 finish: superfluid_abi::finish::LENGTH,
1373 expired: false,
1374 });
1375 }
1376 if deadline.is_some_and(|at| std::time::Instant::now() >= at) {
1377 return Ok(FleetGeneration {
1378 tokens: all_tokens,
1379 finish: crate::nodeagent::FINISH_EXPIRED,
1380 expired: true,
1381 });
1382 }
1383 let cand: Vec<usize> = self
1384 .candidates_claiming(
1385 &placed.model,
1386 Some(session),
1387 Self::replacement_shape(&input, remaining),
1388 )
1389 .into_iter()
1390 .filter(|i| !tried.contains(i))
1391 .collect();
1392 if cand.is_empty() {
1393 return Err(DaemonError::Generation(
1394 "streaming generation failed and no healthy node could take over".into(),
1395 ));
1396 }
1397 let i = cand[0];
1398 tried.push(i);
1399 let mut ctx = input.clone();
1400 ctx.extend_from_slice(&all_tokens);
1401 match self.try_place_by(
1402 i,
1403 session,
1404 placed.params,
1405 placed.qos_class,
1406 &placed.codec,
1407 &placed.model,
1408 &ctx,
1409 deadline,
1410 ) {
1411 Ok(true) => {}
1412 Ok(false) => continue,
1413 Err(e) => {
1414 self.mark_unhealthy(i, &e);
1415 self.node(i).reset();
1416 continue;
1417 }
1418 }
1419 tracing::warn!(
1420 "fleet: session {session} moved to {} after {} streamed tokens",
1421 self.identity(i),
1422 all_tokens.len()
1423 );
1424 match self.generate_streaming_on(i, session, remaining, max_wall_ms, deadline, &flag, opts, on_logprobs, |b| {
1425 all_tokens.extend_from_slice(b);
1426 on_tokens(b);
1427 }) {
1428 Ok(g) => {
1429 return Ok(FleetGeneration {
1430 tokens: all_tokens,
1431 finish: g.finish,
1432 expired: g.expired,
1433 })
1434 }
1435 Err(e) if !is_fatal_node(&e) => return Err(e),
1436 Err(e) => self.mark_unhealthy(i, &e),
1437 }
1438 }
1439 }
1440
1441 #[allow(clippy::too_many_arguments)]
1442 fn generate_streaming_on(
1443 &self,
1444 node: usize,
1445 session: u64,
1446 max_new_tokens: u64,
1447 max_wall_ms: u64,
1448 deadline: Option<std::time::Instant>,
1449 flag: &AtomicBool,
1450 opts: Option<&crate::fleet::Ask<'_>>,
1451 on_logprobs: &mut dyn FnMut(&[superfluid_proto::linkf::TokenLogprobMsg]),
1452 mut on_tokens: impl FnMut(&[u32]),
1453 ) -> Result<FleetGeneration, DaemonError> {
1454 let _inflight = InFlight::guard(&self.shared, node, session);
1455 let node_conn = self.node(node);
1456 let unstarted = || FleetGeneration { tokens: Vec::new(), finish: crate::nodeagent::FINISH_EXPIRED, expired: true };
1459 let Some(mut guard) = Self::checkout_live_by(&node_conn, self.slot_of(session), deadline) else {
1460 return Ok(unstarted());
1461 };
1462 let max_wall_ms = match deadline.map(|at| at.saturating_duration_since(std::time::Instant::now())) {
1463 Some(left) if left.is_zero() => return Ok(unstarted()),
1464 Some(left) => max_wall_ms.min(left.as_millis().max(1) as u64),
1465 None => max_wall_ms,
1466 };
1467 self.ensure_connected(node, &mut guard)?;
1468 self.rebind_on(session, &mut guard)?;
1469 let head = guard.as_mut().expect("connected");
1470 let decoded = &node_conn.decoded;
1471 let r = head.generate_streaming_with(session, max_new_tokens, max_wall_ms, flag, opts, on_logprobs, |b: &[u32]| {
1472 decoded.fetch_add(b.len() as u64, Ordering::Relaxed);
1473 on_tokens(b);
1474 });
1475 if let Err(e) = &r {
1476 if is_fatal_node(e) {
1477 *guard = None;
1478 }
1479 }
1480 if let Some(head) = guard.as_ref() {
1481 self.sync_meta(node, head);
1482 }
1483 drop(guard);
1484 self.map_refusal(node, session, r)
1485 }
1486
1487 pub fn state_sync(
1490 &self,
1491 model: &str,
1492 req: superfluid_proto::linkw::StateSyncReqMsg,
1493 blobs: &[crate::fleet::Blob],
1494 ) -> Result<superfluid_proto::linkw::StateSyncOkMsg, DaemonError> {
1495 self.on_a_node(model, |head| head.state_sync(req.clone(), blobs))
1496 }
1497
1498 pub fn state_sync_streaming(
1502 &self,
1503 model: &str,
1504 req: superfluid_proto::linkw::StateSyncReqMsg,
1505 blobs: &[crate::fleet::Blob],
1506 on_segment: &mut dyn FnMut(superfluid_proto::linkw::TranscribeSegmentMsg),
1507 ) -> Result<superfluid_proto::linkw::StateSyncOkMsg, DaemonError> {
1508 let mut told = false;
1509 self.on_a_node(model, |head| {
1510 let answer = head.state_sync_streaming(req.clone(), blobs, &mut |seg| {
1511 told = true;
1512 on_segment(seg);
1513 });
1514 match answer {
1515 Err(e) if told && is_fatal_node(&e) => Err(DaemonError::Generation(format!("the node transcribing was lost: {e}"))),
1516 answer => answer,
1517 }
1518 })
1519 }
1520
1521 pub fn state_sync_all(
1524 &self,
1525 model: &str,
1526 req: superfluid_proto::linkw::StateSyncReqMsg,
1527 ) -> Result<superfluid_proto::linkw::StateSyncOkMsg, DaemonError> {
1528 let cand = self.candidates_claiming(model, None, RequestShape::default());
1529 let mut answer = Err(DaemonError::NoNode("no healthy node can serve the model"));
1530 for i in cand {
1531 answer = Ok(self.state_sync_on(i, req.clone(), &[])?);
1532 }
1533 answer
1534 }
1535
1536 fn state_sync_on(
1537 &self,
1538 i: usize,
1539 req: superfluid_proto::linkw::StateSyncReqMsg,
1540 blobs: &[crate::fleet::Blob],
1541 ) -> Result<superfluid_proto::linkw::StateSyncOkMsg, DaemonError> {
1542 self.on_node(i, |head| head.state_sync(req, blobs))
1543 }
1544
1545 fn on_node<R>(&self, i: usize, call: impl FnOnce(&mut FleetHead) -> Result<R, DaemonError>) -> Result<R, DaemonError> {
1548 let node = self.node(i);
1549 let mut guard = node.checkout_idle();
1550 if guard.as_mut().is_some_and(|h| h.drain_status().is_err()) {
1551 *guard = None;
1552 }
1553 self.ensure_connected(i, &mut guard)?;
1554 let r = call(guard.as_mut().expect("connected"));
1555 if let Err(e) = &r {
1556 if is_fatal_node(e) {
1557 *guard = None;
1558 self.mark_unhealthy(i, e);
1559 }
1560 }
1561 r
1562 }
1563
1564 fn on_a_node<R>(&self, model: &str, mut call: impl FnMut(&mut FleetHead) -> Result<R, DaemonError>) -> Result<R, DaemonError> {
1566 let cand = self.candidates_claiming(model, None, RequestShape::default());
1567 let mut last = None;
1568 for i in cand {
1569 match self.on_node(i, &mut call) {
1570 Ok(r) => return Ok(r),
1571 Err(e) if is_fatal_node(&e) => last = Some(e),
1572 Err(e) => return Err(e),
1573 }
1574 }
1575 Err(last.unwrap_or(DaemonError::NoNode("no healthy node can serve the model")))
1576 }
1577
1578 pub fn remote_tokenizer(self: &Arc<Self>, model: &str) -> Result<crate::fleet::RemoteTokenizer, DaemonError> {
1581 let export = self.on_a_node(model, |head| head.tokenizer_export())?;
1582 if export.vocab.is_empty() {
1583 return Err(DaemonError::Generation(format!(
1584 "no node serving {model} shares a tokenizer; give the head --model a path to the model's files"
1585 )));
1586 }
1587 Ok(crate::fleet::RemoteTokenizer::new(Arc::clone(self), model.to_string(), export))
1588 }
1589
1590 pub fn tokenize(&self, model: &str, text: &superfluid_proto::linkf::TokenizeMsg) -> Result<Option<Vec<u32>>, DaemonError> {
1592 self.on_a_node(model, |head| head.tokenize(text.clone()))
1593 }
1594
1595 pub fn cancel(&self, session: u64) -> Result<(), DaemonError> {
1596 let sh = self.shared.lock().expect("shared");
1597 if !sh.sessions.contains_key(&session) {
1598 return Err(DaemonError::UnknownSession(session));
1599 }
1600 if let Some(flag) = sh.cancels.get(&session) {
1601 flag.store(true, Ordering::Relaxed);
1602 }
1603 Ok(())
1604 }
1605
1606 pub fn park(&self, session: u64) -> Result<(), DaemonError> {
1607 let node = {
1608 let mut sh = self.shared.lock().expect("shared");
1609 let p = sh
1610 .sessions
1611 .get_mut(&session)
1612 .ok_or(DaemonError::UnknownSession(session))?;
1613 if p.parked {
1614 return Ok(());
1615 }
1616 p.parked = true;
1617 (p.node, p.slot, p.epoch)
1618 };
1619 let (node, slot, epoch) = node;
1620 let node_conn = self.node(node);
1621 let mut guard = Self::checkout_live(&node_conn, slot);
1622 if self.ensure_connected(node, &mut guard).is_ok() {
1623 if let Err(e) = guard.as_mut().expect("connected").revoke(session, epoch) {
1624 *guard = None;
1625 self.mark_unhealthy(node, &e);
1626 }
1627 }
1628 Ok(())
1629 }
1630
1631 pub fn resume(&self, session: u64) -> Result<usize, DaemonError> {
1632 let placed = {
1633 let sh = self.shared.lock().expect("shared");
1634 let p = sh
1635 .sessions
1636 .get(&session)
1637 .ok_or(DaemonError::UnknownSession(session))?;
1638 if !p.parked {
1639 return Err(DaemonError::Config("session is not parked"));
1640 }
1641 p.clone()
1642 };
1643 let cand = self.candidates_claiming(
1644 &placed.model,
1645 Some(session),
1646 Self::replacement_shape(&placed.tokens, 0),
1647 );
1648 if cand.is_empty() {
1649 InFlight::release(&self.shared, session);
1650 return Err(DaemonError::NoNode("no healthy node can serve the model"));
1651 }
1652 let mut last_err = None;
1653 for i in cand {
1654 match self.try_place_on(
1655 i,
1656 session,
1657 placed.params,
1658 placed.qos_class,
1659 &placed.codec,
1660 &placed.model,
1661 &placed.tokens,
1662 ) {
1663 Ok(true) => return Ok(i),
1664 Ok(false) => continue,
1665 Err(e) => {
1666 self.mark_unhealthy(i, &e);
1667 self.node(i).reset();
1668 last_err = Some(e);
1669 }
1670 }
1671 }
1672 Err(last_err.unwrap_or(DaemonError::NoNode("no reachable node serves the model")))
1673 }
1674
1675 pub fn finish(&self, session: u64) {
1678 let placed = {
1679 let mut sh = self.shared.lock().expect("shared");
1680 sh.cancels.remove(&session);
1681 sh.sessions.remove(&session)
1682 };
1683 InFlight::release(&self.shared, session);
1684 let Some(p) = placed else { return };
1685 let identity = self.shared.lock().expect("shared").meta[p.node].identity.clone();
1686 if !identity.is_empty() {
1687 self.warm.lock().expect("warm").ran(&p.model, &p.tokens, &identity);
1688 }
1689 self.shared.lock().expect("shared").forgets.push((p.node, p.slot, session, p.epoch));
1690 let node = self.node(p.node);
1691 let lock = node.conns[p.slot].try_lock();
1692 if let Ok(mut guard) = lock {
1693 if let Some(head) = guard.as_mut() {
1694 self.flush_forgets(p.node, p.slot, head);
1695 }
1696 }
1697 }
1698
1699 fn flush_forgets(&self, node: usize, slot: usize, head: &mut FleetHead) {
1700 let due: Vec<(u64, u64)> = {
1701 let mut sh = self.shared.lock().expect("shared");
1702 if sh.forgets.is_empty() {
1703 return;
1704 }
1705 let (due, keep) = sh.forgets.drain(..).partition(|&(n, s, _, _)| n == node && s == slot);
1706 sh.forgets = keep;
1707 due.into_iter().map(|(_, _, session, epoch)| (session, epoch)).collect()
1708 };
1709 for (session, epoch) in due {
1710 let _ = head.revoke(session, epoch);
1711 }
1712 }
1713
1714 pub fn probe(&self) -> Vec<Result<NodeReport, DaemonError>> {
1716 std::thread::scope(|scope| {
1717 let jobs: Vec<_> = (0..self.node_count())
1718 .map(|i| {
1719 scope.spawn(move || {
1720 let node = self.node(i);
1721 let mut guard = node.checkout_at(0);
1722 self.ensure_connected(i, &mut guard)?;
1723 let node = guard.as_ref().expect("connected").node();
1724 Ok(NodeReport {
1725 addr: self.node(i).spec.addr.clone(),
1726 identity: node.node_identity.clone(),
1727 models: node.loadable_models.clone(),
1728 max_context_tokens: node.capacity.max_context_tokens,
1729 max_lanes: node.capacity.max_lanes,
1730 })
1731 })
1732 })
1733 .collect();
1734 jobs.into_iter().map(|j| j.join().expect("probe thread")).collect()
1735 })
1736 }
1737
1738 pub fn keepalive(&self) {
1742 let nodes: Vec<Arc<NodeConn>> = self.nodes.read().expect("nodes").clone();
1743 for (i, node) in nodes.iter().enumerate() {
1744 let mut dropped = None;
1745 for (slot, conn) in node.conns.iter().enumerate() {
1746 let Ok(mut guard) = conn.try_lock() else { continue };
1747 let Some(head) = guard.as_mut() else { continue };
1748 self.flush_forgets(i, slot, head);
1749 match head.drain_status() {
1750 Ok(()) => self.sync_meta(i, head),
1751 Err(e) => {
1752 *guard = None;
1753 dropped = Some(e);
1754 }
1755 }
1756 }
1757 if let Ok(mut guard) = node.control.try_lock() {
1758 if guard.as_mut().is_some_and(|h| h.drain_status().is_err()) {
1759 *guard = None;
1760 }
1761 }
1762 if let Some(e) = dropped {
1766 if node.conns.iter().all(|c| c.try_lock().is_ok_and(|g| g.is_none())) {
1767 self.mark_unhealthy(i, &e);
1768 }
1769 }
1770 }
1771 }
1772
1773 pub fn retry_down(&self) {
1776 let due: Vec<usize> = {
1777 let mut sh = self.shared.lock().expect("shared");
1778 let now = std::time::Instant::now();
1779 let mut due = Vec::new();
1780 for (i, m) in sh.meta.iter_mut().enumerate() {
1781 if m.down_since.is_some() && m.last_retry.is_none_or(|t| now.duration_since(t) >= self.retry_every) {
1782 m.last_retry = Some(now);
1783 due.push(i);
1784 }
1785 }
1786 due
1787 };
1788 std::thread::scope(|scope| {
1789 for i in due {
1790 scope.spawn(move || {
1791 let node = self.node(i);
1792 let spec = &node.spec;
1793 if spec.addr.starts_with(JOINED) {
1795 return;
1796 }
1797 match FleetHead::connect(&spec.addr, spec.auth.clone()) {
1798 Ok(mut head) => {
1799 head.keep_alive(self.liveness);
1800 watch(&node, &mut head);
1801 self.sync_meta(i, &head);
1802 self.mark_up(i);
1803 if let Ok(mut slot) = node.conns[0].try_lock() {
1804 if slot.is_none() {
1805 *slot = Some(head);
1806 }
1807 }
1808 }
1809 Err(e) => tracing::debug!("fleet: {e}"),
1810 }
1811 });
1812 }
1813 });
1814 }
1815
1816 pub fn tend(&self) {
1817 self.keepalive();
1818 self.retry_down();
1819 self.warm.lock().expect("warm").save();
1820 }
1821
1822 fn warm_node(&self, model: &str, tokens: &[u32]) -> Option<usize> {
1824 let warm = self.warm.lock().expect("warm");
1825 let (identity, _) = warm.warmest(model, tokens)?;
1826 let sh = self.shared.lock().expect("shared");
1827 (0..sh.meta.len()).find(|&i| sh.meta[i].identity == identity && sh.meta[i].up() && Self::serves(&sh, i, model))
1828 }
1829
1830 pub fn spawn_keepalive(self: &Arc<Self>) {
1831 let mgr = Arc::downgrade(self);
1832 let tick = self.retry_every.min(self.liveness.heartbeat);
1833 std::thread::Builder::new()
1834 .name("fleet-keepalive".into())
1835 .spawn(move || loop {
1836 std::thread::sleep(tick);
1837 let Some(mgr) = mgr.upgrade() else { return };
1838 mgr.tend();
1839 })
1840 .expect("spawn the fleet keepalive");
1841 }
1842}
1843
1844fn in_use(slot: &Mutex<Option<FleetHead>>) -> bool {
1848 slot.try_lock().map_or(true, |g| g.is_some())
1849}
1850
1851fn is_fatal_node(e: &DaemonError) -> bool {
1852 matches!(
1853 e,
1854 DaemonError::FleetNode { .. }
1855 | DaemonError::Fleet(superfluid_linkf::LinkFError::Closed)
1856 | DaemonError::Fleet(superfluid_linkf::LinkFError::TimedOut)
1857 | DaemonError::Fleet(superfluid_linkf::LinkFError::Io(_))
1858 )
1859}