1use std::collections::BTreeMap;
4
5use superfluid_linkf::{Endpoint, HeadSession, Liveness};
6use superfluid_proto::handshake::HelloAckF;
7use superfluid_proto::linkf::{
8 self, msg_type, AppendContentMsg, GenerateCancelMsg, GenerationBudgetsMsg, Lease, TokenEventPayload,
9};
10
11use crate::codec::TextCodec;
12use superfluid_engine::StageOutput;
13use crate::wal::GenParams;
14use crate::DaemonError;
15
16pub fn encode_params(params: &GenParams) -> Vec<u8> {
17 postcard::to_stdvec(params).expect("GenParams postcard")
18}
19
20pub fn decode_params(bytes: &[u8]) -> GenParams {
21 postcard::from_bytes(bytes).unwrap_or_default()
22}
23
24pub fn encode_tokens(tokens: &[u32]) -> Vec<u8> {
25 postcard::to_stdvec(&tokens.to_vec()).expect("tokens postcard")
26}
27
28pub fn decode_tokens(bytes: &[u8]) -> Result<Vec<u32>, DaemonError> {
29 postcard::from_bytes(bytes).map_err(DaemonError::Codec)
30}
31
32#[derive(Debug, Clone)]
33pub struct FleetGeneration {
34 pub tokens: Vec<u32>,
35 pub finish: u32,
36 pub expired: bool,
37}
38
39#[derive(Debug, Clone)]
40pub struct FleetSegment {
41 pub channel: u32,
42 pub text: String,
43 pub tool: Option<(String, String)>,
44}
45
46#[derive(Debug, Clone)]
47struct Placement {
48 params: GenParams,
49 qos_class: u8,
50 codec: String,
51 lease: Lease,
52 tokens: Vec<u32>,
53}
54
55pub fn tokenizer_digest(tok: &dyn superfluid_engine::Tokenizer) -> u64 {
58 use sha2::Digest;
59 let mut h = sha2::Sha256::new();
60 let n = tok.vocab_size();
61 h.update(n.to_le_bytes());
62 for t in 0..n {
63 let bytes = tok.token_bytes(t);
64 h.update((bytes.len() as u32).to_le_bytes());
65 h.update(&bytes);
66 }
67 let d = h.finalize();
68 u64::from_le_bytes(d[..8].try_into().expect("8 bytes")).max(1)
69}
70
71pub struct RemoteTokenizer {
76 fleet: std::sync::Arc<crate::fleet_manager::FleetManager>,
77 model: String,
78 export: linkf::TokenizerExportMsg,
79 encoded: std::sync::Mutex<Encoded>,
80}
81
82#[derive(Default)]
85struct Encoded {
86 by_text: std::collections::HashMap<[u8; 32], (Vec<u32>, u64)>,
87 tokens: usize,
88 clock: u64,
89}
90
91const ENCODED_TOKENS: usize = 2_000_000;
92
93impl Encoded {
94 fn get(&mut self, key: &[u8; 32]) -> Option<Vec<u32>> {
95 self.clock += 1;
96 let clock = self.clock;
97 self.by_text.get_mut(key).map(|(tokens, at)| {
98 *at = clock;
99 tokens.clone()
100 })
101 }
102
103 fn put(&mut self, key: [u8; 32], tokens: Vec<u32>) {
104 self.clock += 1;
105 self.tokens += tokens.len();
106 if let Some((old, _)) = self.by_text.insert(key, (tokens, self.clock)) {
107 self.tokens -= old.len();
108 }
109 if self.tokens > ENCODED_TOKENS {
110 let mut ages: Vec<([u8; 32], u64, usize)> = self.by_text.iter().map(|(k, (t, at))| (*k, *at, t.len())).collect();
111 ages.sort_unstable_by_key(|&(_, at, _)| at);
112 for (k, _, n) in ages {
113 if self.tokens <= ENCODED_TOKENS / 2 {
114 break;
115 }
116 self.by_text.remove(&k);
117 self.tokens -= n;
118 }
119 }
120 }
121}
122
123impl RemoteTokenizer {
124 pub fn new(fleet: std::sync::Arc<crate::fleet_manager::FleetManager>, model: String, export: linkf::TokenizerExportMsg) -> RemoteTokenizer {
125 RemoteTokenizer { fleet, model, export, encoded: Default::default() }
126 }
127
128 pub fn digest(&self) -> u64 {
130 self.export.digest
131 }
132
133 fn encode_by(&self, text: linkf::TokenizeMsg) -> Option<Vec<u32>> {
134 use sha2::Digest;
135 let key: [u8; 32] = sha2::Sha256::digest(postcard::to_allocvec(&text).ok()?).into();
136 if let Some(tokens) = self.encoded.lock().expect("encoded").get(&key) {
137 return Some(tokens);
138 }
139 match self.fleet.tokenize(&self.model, &text) {
140 Ok(Some(tokens)) => {
141 self.encoded.lock().expect("encoded").put(key, tokens.clone());
142 Some(tokens)
143 }
144 Ok(tokens) => tokens,
145 Err(e) => {
146 tracing::warn!("fleet: no node serving {} could encode a request's text: {e}", self.model);
147 None
148 }
149 }
150 }
151}
152
153impl superfluid_engine::Tokenizer for RemoteTokenizer {
154 fn encode(&self, text: &str) -> Vec<u32> {
155 self.encode_by(linkf::TokenizeMsg::Encode(text.to_string())).unwrap_or_default()
156 }
157
158 fn token_bytes(&self, token: u32) -> Vec<u8> {
159 self.export.vocab.get(token as usize).cloned().unwrap_or_default()
160 }
161
162 fn vocab_size(&self) -> u32 {
163 self.export.vocab.len() as u32
164 }
165
166 fn special_tokens(&self) -> Vec<(String, u32)> {
167 self.export.specials.clone()
168 }
169
170 fn bos_token(&self) -> Option<u32> {
171 self.export.bos
172 }
173
174 fn eos_token(&self) -> u32 {
175 self.export.eos
176 }
177
178 fn chat_template_jinja(&self) -> String {
179 self.export.chat_template.clone()
180 }
181
182 fn chat_template_named(&self, name: &str) -> Option<String> {
183 self.export.named_templates.iter().find(|(n, _)| n == name).map(|(_, t)| t.clone())
184 }
185
186 fn encode_plain(&self, text: &str) -> Option<Vec<u32>> {
187 self.export.plain.then(|| self.encode_by(linkf::TokenizeMsg::Plain(text.to_string()))).flatten()
188 }
189
190 fn encode_pieces(&self, pieces: &[(&str, bool)]) -> Option<Vec<u32>> {
191 let pieces = pieces.iter().map(|(p, s)| (p.to_string(), *s)).collect();
192 self.export.pieces.then(|| self.encode_by(linkf::TokenizeMsg::Pieces(pieces))).flatten()
193 }
194}
195
196#[derive(Debug, Clone)]
198pub struct Blob {
199 pub hash: String,
200 pub path: std::path::PathBuf,
201}
202
203pub struct Ask<'a> {
205 pub opts: &'a linkf::GenerateOptsMsg,
206 pub blobs: &'a [Blob],
207}
208
209const BLOB_CHUNK: usize = 4 << 20;
211
212pub struct FleetHead {
213 head: HeadSession,
214 endpoint: Endpoint,
215 node: HelloAckF,
216 next_generation: u64,
217 placements: BTreeMap<u64, Placement>,
218 addr: String,
219 auth: Vec<u8>,
220 on_status: Option<StatusHook>,
221 liveness: Liveness,
222 blobs_sent: std::collections::HashSet<String>,
224 tokenizer: u64,
226}
227
228pub type StatusHook = std::sync::Arc<dyn Fn(&superfluid_proto::handshake::CapacitySummary) + Send + Sync>;
230
231static NEXT_EPOCH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
234static NEXT_COMMANDS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
235
236fn fresh_session_state() -> HeadSession {
237 HeadSession::starting_at(NEXT_COMMANDS.fetch_add(1 << 32, std::sync::atomic::Ordering::Relaxed))
238}
239
240pub const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
241
242pub const HANDSHAKE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
243
244fn closed_at_handshake() -> String {
245 "closed the connection at the handshake; check that --fleet-auth holds the token in the node's --auth-file".into()
246}
247
248fn handshake(addr: &str, auth: Vec<u8>) -> Result<(Endpoint, HelloAckF), DaemonError> {
250 let node = |why: String| DaemonError::FleetNode { addr: addr.to_string(), why };
251 let stream = Endpoint::tcp_connect_within(addr, CONNECT_TIMEOUT).map_err(|e| node(format!("unreachable ({e})")))?;
252 let opened = if auth.is_empty() {
253 Endpoint::from_tcp(stream)
254 } else {
255 Endpoint::from_tcp_secure(stream, &auth, superfluid_linkf::Role::Initiator, HANDSHAKE_TIMEOUT)
256 };
257 hello(opened, addr, auth, "--fleet-auth and the node's --auth-file differ")
258}
259
260fn hello(opened: Result<Endpoint, superfluid_linkf::LinkFError>, peer: &str, auth: Vec<u8>, mismatch: &str) -> Result<(Endpoint, HelloAckF), DaemonError> {
264 use superfluid_linkf::LinkFError;
265 let node = |why: String| DaemonError::FleetNode { addr: peer.to_string(), why };
266 let mut endpoint = match opened {
267 Ok(ep) => ep,
268 Err(LinkFError::Unauthorized) => return Err(node(format!("refused the fleet token: {mismatch}"))),
269 Err(LinkFError::TimedOut) => {
270 return Err(node(format!("did not finish the handshake within {} s", HANDSHAKE_TIMEOUT.as_secs())))
271 }
272 Err(LinkFError::Closed | LinkFError::Io(_)) if !auth.is_empty() => return Err(node(closed_at_handshake())),
273 Err(e) => return Err(node(e.to_string())),
274 };
275 match endpoint.hello_as_head_within(auth, HANDSHAKE_TIMEOUT) {
276 Ok(ack) => Ok((endpoint, ack)),
277 Err(LinkFError::Unauthorized) => Err(node(format!("refused the fleet token: {mismatch}"))),
278 Err(LinkFError::Closed | LinkFError::Io(_)) => Err(node(closed_at_handshake())),
279 Err(LinkFError::TimedOut) => Err(node(format!(
280 "did not answer the handshake within {} s; is superfluid-noded listening there?",
281 HANDSHAKE_TIMEOUT.as_secs()
282 ))),
283 Err(e) => Err(node(e.to_string())),
284 }
285}
286
287fn is_transient(e: &DaemonError) -> bool {
288 matches!(
289 e,
290 DaemonError::Fleet(superfluid_linkf::LinkFError::Closed)
291 | DaemonError::Fleet(superfluid_linkf::LinkFError::TimedOut)
292 | DaemonError::Fleet(superfluid_linkf::LinkFError::Io(_))
293 )
294}
295
296impl FleetHead {
297 pub fn connect(addr: &str, auth: Vec<u8>) -> Result<FleetHead, DaemonError> {
298 let (endpoint, node) = handshake(addr, auth.clone())?;
299 let mut head = FleetHead::from_parts(endpoint, node, addr.to_string(), auth);
300 head.read_tokenizer()?;
301 Ok(head)
302 }
303
304 pub fn accept(sock: std::net::TcpStream, auth: Vec<u8>) -> Result<FleetHead, DaemonError> {
306 let peer = sock.peer_addr().map(|a| a.to_string()).unwrap_or_default();
307 let opened = if auth.is_empty() {
308 Endpoint::from_tcp(sock)
309 } else {
310 Endpoint::from_tcp_secure(sock, &auth, superfluid_linkf::Role::Initiator, HANDSHAKE_TIMEOUT)
311 };
312 let (endpoint, node) = hello(opened, &peer, auth.clone(), "its token differs from this head's")?;
313 let mut head = FleetHead::from_parts(endpoint, node, String::new(), auth);
314 head.read_tokenizer()?;
315 Ok(head)
316 }
317
318 fn from_parts(mut endpoint: Endpoint, node: HelloAckF, addr: String, auth: Vec<u8>) -> FleetHead {
319 endpoint.keep_alive(Liveness::DEFAULT);
320 FleetHead {
321 head: fresh_session_state(),
322 endpoint,
323 node,
324 next_generation: 1,
325 placements: BTreeMap::new(),
326 addr,
327 auth,
328 on_status: None,
329 liveness: Liveness::DEFAULT,
330 blobs_sent: std::collections::HashSet::new(),
331 tokenizer: 0,
332 }
333 }
334
335 pub fn tokenizer(&self) -> u64 {
337 self.tokenizer
338 }
339
340 fn read_tokenizer(&mut self) -> Result<(), DaemonError> {
342 if self.endpoint.version() < 3 {
343 return Ok(());
344 }
345 let by = std::time::Instant::now() + HANDSHAKE_TIMEOUT;
346 loop {
347 for frame in self.endpoint.pump()? {
348 if frame.msg_type == msg_type::TOKENIZER {
349 self.tokenizer = postcard::from_bytes::<linkf::TokenizerMsg>(&frame.payload)?.digest;
350 return Ok(());
351 }
352 self.absorb(frame)?;
353 }
354 if std::time::Instant::now() >= by {
355 return Err(superfluid_linkf::LinkFError::TimedOut.into());
356 }
357 }
358 }
359
360 fn send_blobs(&mut self, blobs: &[Blob]) -> Result<(), DaemonError> {
362 for blob in blobs {
363 if self.blobs_sent.contains(&blob.hash) {
364 continue;
365 }
366 let bytes = std::fs::read(&blob.path).map_err(DaemonError::Io)?;
367 let mut content_hash = [0u8; 32];
368 for (i, b) in content_hash.iter_mut().enumerate() {
369 *b = u8::from_str_radix(blob.hash.get(i * 2..i * 2 + 2).unwrap_or("00"), 16).unwrap_or(0);
370 }
371 let transfer_id = self.next_generation;
372 self.next_generation += 1;
373 self.endpoint.send(
374 msg_type::XFER_BEGIN,
375 &linkf::XferBeginMsg { transfer_id, total_len: bytes.len() as u64, checksum: 0, content_type: 0, content_hash },
376 )?;
377 for (i, chunk) in bytes.chunks(BLOB_CHUNK).enumerate() {
378 let msg = linkf::XferChunkMsg { transfer_id, offset: (i * BLOB_CHUNK) as u64, data: chunk.to_vec() };
379 self.endpoint.send(msg_type::XFER_CHUNK, &msg)?;
380 }
381 self.endpoint.send(msg_type::XFER_END, &linkf::XferEndMsg { transfer_id })?;
382 self.blobs_sent.insert(blob.hash.clone());
383 }
384 Ok(())
385 }
386
387 fn grace(&self) -> std::time::Duration {
390 match self.liveness.silence {
391 t if t.is_zero() => Liveness::DEFAULT.silence,
392 t => t,
393 }
394 }
395
396 fn wall_deadline(&self, max_wall_ms: u64) -> Option<std::time::Instant> {
399 (max_wall_ms > 0)
400 .then(|| std::time::Instant::now() + std::time::Duration::from_millis(max_wall_ms) + self.grace())
401 }
402
403 fn frames_within(&mut self, within: std::time::Duration) -> Result<Vec<superfluid_proto::Frame>, DaemonError> {
405 let by = std::time::Instant::now() + within;
406 loop {
407 let frames = self.endpoint.pump()?;
408 if !frames.is_empty() {
409 return Ok(frames);
410 }
411 if std::time::Instant::now() >= by {
412 return Err(superfluid_linkf::LinkFError::TimedOut.into());
413 }
414 }
415 }
416
417 pub fn keep_alive(&mut self, liveness: Liveness) {
419 self.liveness = liveness;
420 self.endpoint.keep_alive(liveness);
421 }
422
423 pub fn on_status(&mut self, hook: StatusHook) {
426 self.on_status = Some(hook);
427 }
428
429 fn reconnect_self(&mut self) -> Result<(), DaemonError> {
430 let (addr, auth) = (self.addr.clone(), self.auth.clone());
431 self.reconnect(&addr, auth)?;
432 Ok(())
433 }
434
435 pub fn node(&self) -> &HelloAckF {
436 &self.node
437 }
438
439 pub fn ask_control(&mut self) -> Result<(), DaemonError> {
443 self.endpoint.send(msg_type::CONTROL_ASK, &())?;
444 Ok(())
445 }
446
447 pub fn drain_status(&mut self) -> Result<(), DaemonError> {
448 for frame in self.endpoint.pump()? {
449 self.absorb(frame)?;
450 }
451 Ok(())
452 }
453
454 fn absorb(&mut self, frame: superfluid_proto::Frame) -> Result<Option<superfluid_proto::Frame>, DaemonError> {
457 match frame.msg_type {
458 msg_type::NODE_STATUS => {
459 if let Ok(m) = postcard::from_bytes::<linkf::NodeStatusMsg>(&frame.payload) {
460 self.node.capacity = m.capacity;
461 if let Some(hook) = &self.on_status {
462 hook(&self.node.capacity);
463 }
464 if !m.loadable_models.is_empty() {
465 self.node.loadable_models = m.loadable_models;
466 }
467 }
468 Ok(None)
469 }
470 msg_type::LEASE_RENEW => {
471 let m: linkf::LeaseRenewMsg = postcard::from_bytes(&frame.payload)?;
472 if let Some(grant) = self.head.on_lease_renew(&m) {
473 self.endpoint.send(msg_type::LEASE_GRANT, &grant)?;
474 }
475 Ok(None)
476 }
477 _ => Ok(Some(frame)),
478 }
479 }
480
481 pub fn knows(&self, session_id: u64) -> bool {
482 self.head.current_epoch(session_id).is_some()
483 }
484
485
486 pub fn reconnect(&mut self, addr: &str, auth: Vec<u8>) -> Result<Vec<u64>, DaemonError> {
487 let _ = self.endpoint.shutdown();
488 if addr.is_empty() {
489 return Err(DaemonError::FleetNode {
490 addr: self.node.node_identity.clone(),
491 why: "dialed in on its own; waiting for it to join again".into(),
492 });
493 }
494 let (mut endpoint, node) = handshake(addr, auth)?;
495 endpoint.keep_alive(self.liveness);
496 self.node = node;
497 self.endpoint = endpoint;
498 self.blobs_sent.clear();
499 self.read_tokenizer()?;
500 let rec = self.head.reconcile();
501 self.endpoint.send(msg_type::RECONCILE, &rec)?;
502
503 let mut received: std::collections::BTreeSet<(u64, u64, u64)> =
504 std::collections::BTreeSet::new();
505 let ack = loop {
506 let mut got = None;
507 for frame in self.frames_within(self.grace())? {
508 let Some(frame) = self.absorb(frame)? else { continue };
509 match frame.msg_type {
510 msg_type::RECONCILE_ACK => {
511 got = Some(postcard::from_bytes::<linkf::ReconcileAckMsg>(&frame.payload)?);
512 }
513 msg_type::TOKEN_EVENTS => {
514 let m: linkf::TokenEventsMsg = postcard::from_bytes(&frame.payload)?;
515 for ev in &m.events {
516 received.insert((ev.session_id, ev.generation_id, ev.event_seq));
517 }
518 for a in self.head.on_token_events(&m) {
519 self.endpoint.send(msg_type::WATERMARK_ACK, &a)?;
520 }
521 }
522 _ => {}
523 }
524 }
525 if let Some(a) = got {
526 break a;
527 }
528 };
529
530 let mut expected: std::collections::BTreeSet<(u64, u64, u64)> =
531 std::collections::BTreeSet::new();
532 for s in &ack.sessions {
533 if self.head.current_epoch(s.session_id).is_some() {
534 for r in &s.retry_ranges {
535 for seq in r.from_seq..=r.to_seq {
536 let id = (s.session_id, r.generation_id, seq);
537 if !received.contains(&id) {
538 expected.insert(id);
539 }
540 }
541 }
542 }
543 }
544 while !expected.is_empty() {
545 for frame in self.frames_within(self.grace())? {
546 let Some(frame) = self.absorb(frame)? else { continue };
547 if frame.msg_type != msg_type::TOKEN_EVENTS {
548 continue;
549 }
550 let m: linkf::TokenEventsMsg = postcard::from_bytes(&frame.payload)?;
551 for ev in &m.events {
552 expected.remove(&(ev.session_id, ev.generation_id, ev.event_seq));
553 }
554 for a in self.head.on_token_events(&m) {
555 self.endpoint.send(msg_type::WATERMARK_ACK, &a)?;
556 }
557 }
558 }
559
560 let reassigned = self.head.on_reconcile_ack(&ack);
563 for s in ack.sessions.iter().filter(|s| s.held && !reassigned.contains(&s.session_id)) {
564 if let (Some(p), Some(epoch)) =
565 (self.placements.get(&s.session_id).cloned(), self.head.current_epoch(s.session_id))
566 {
567 if !p.tokens.is_empty() {
568 self.send_append(s.session_id, epoch, &p.tokens)?;
569 }
570 }
571 }
572 let mut replaced = Vec::new();
573 for session_id in reassigned {
574 if let Some(p) = self.placements.get(&session_id).cloned() {
575 if let Some(epoch) = self.head.current_epoch(session_id) {
576 let mut msg = self.head.assign_session(
577 session_id,
578 epoch,
579 p.lease,
580 p.qos_class,
581 &p.codec,
582 );
583 msg.params = encode_params(&p.params);
584 self.endpoint.send(msg_type::SESSION_ASSIGN, &msg)?;
585 if !p.tokens.is_empty() {
586 self.send_append(session_id, epoch, &p.tokens)?;
587 }
588 replaced.push(session_id);
589 }
590 }
591 }
592 Ok(replaced)
593 }
594
595 pub fn assign(
596 &mut self,
597 session_id: u64,
598 params: GenParams,
599 qos_class: u8,
600 codec: &str,
601 lease: Lease,
602 ) -> Result<u64, DaemonError> {
603 let entry = self.placements.entry(session_id).or_insert(Placement {
604 params,
605 qos_class,
606 codec: codec.to_string(),
607 lease,
608 tokens: Vec::new(),
609 });
610 entry.params = params;
611 entry.qos_class = qos_class;
612 entry.codec = codec.to_string();
613 entry.lease = lease;
614
615 let epoch = NEXT_EPOCH.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
616 let mut msg = self.head.assign_session(session_id, epoch, lease, qos_class, codec);
617 msg.params = encode_params(¶ms);
618 match self.endpoint.send(msg_type::SESSION_ASSIGN, &msg) {
619 Ok(()) => Ok(epoch),
620 Err(e) => {
621 let e: DaemonError = e.into();
622 if is_transient(&e) {
623 self.reconnect_self()?;
624 Ok(self.head.current_epoch(session_id).unwrap_or(epoch))
625 } else {
626 Err(e)
627 }
628 }
629 }
630 }
631
632 pub fn park(&mut self, session_id: u64) -> Result<(), DaemonError> {
633 let epoch = self.head.current_epoch(session_id).unwrap_or(0);
634 self.revoke(session_id, epoch)
635 }
636
637 pub fn revoke(&mut self, session_id: u64, epoch: u64) -> Result<(), DaemonError> {
640 self.placements.remove(&session_id);
641 let msg = match self.head.revoke(session_id) {
642 Some(msg) => msg,
643 None => linkf::SessionRevokeMsg { command_id: self.head.next_command(session_id), session_id, epoch },
644 };
645 self.endpoint.send(msg_type::SESSION_REVOKE, &msg)?;
646 Ok(())
647 }
648
649 pub fn append(&mut self, session_id: u64, tokens: &[u32]) -> Result<(), DaemonError> {
650 let epoch = self
651 .head
652 .current_epoch(session_id)
653 .ok_or(DaemonError::Protocol("append before assign"))?;
654 if let Some(p) = self.placements.get_mut(&session_id) {
655 p.tokens = tokens.to_vec();
656 }
657 match self.send_append(session_id, epoch, tokens) {
658 Ok(()) => Ok(()),
659 Err(e) if is_transient(&e) => {
660 self.reconnect_self()
661 }
662 Err(e) => Err(e),
663 }
664 }
665
666 fn send_append(&mut self, session_id: u64, epoch: u64, tokens: &[u32]) -> Result<(), DaemonError> {
667 let msg = AppendContentMsg {
668 command_id: self.head.next_command(session_id),
669 session_id,
670 epoch,
671 blocks: encode_tokens(tokens),
672 spans: Vec::new(),
673 media_refs: Vec::new(),
674 };
675 self.endpoint.send(msg_type::APPEND_CONTENT, &msg)?;
676 Ok(())
677 }
678
679 pub fn generate(
680 &mut self,
681 session_id: u64,
682 max_new_tokens: u64,
683 max_wall_ms: u64,
684 ) -> Result<FleetGeneration, DaemonError> {
685 let generation_id = self.next_generation;
686 self.next_generation += 1;
687 for attempt in 0..2 {
688 match self.generate_once(session_id, generation_id, max_new_tokens, max_wall_ms) {
689 Ok(g) => return Ok(g),
690 Err(e) if attempt == 0 && is_transient(&e) => {
691 self.reconnect_self()?;
692 if let Some(g) = self.recovered_generation(session_id, generation_id) {
693 self.head.retire_generation(session_id, generation_id);
694 return Ok(g);
695 }
696 continue;
697 }
698 Err(e) => return Err(e),
699 }
700 }
701 unreachable!("generate retried at most once")
702 }
703
704 pub fn generate_streaming(
705 &mut self,
706 session_id: u64,
707 max_new_tokens: u64,
708 max_wall_ms: u64,
709 cancel: &std::sync::atomic::AtomicBool,
710 on_tokens: impl FnMut(&[u32]),
711 ) -> Result<FleetGeneration, DaemonError> {
712 self.generate_streaming_with(session_id, max_new_tokens, max_wall_ms, cancel, None, &mut |_| {}, on_tokens)
713 }
714
715 #[allow(clippy::too_many_arguments)]
718 pub fn generate_streaming_with(
719 &mut self,
720 session_id: u64,
721 max_new_tokens: u64,
722 max_wall_ms: u64,
723 cancel: &std::sync::atomic::AtomicBool,
724 opts: Option<&Ask<'_>>,
725 on_logprobs: &mut dyn FnMut(&[linkf::TokenLogprobMsg]),
726 mut on_tokens: impl FnMut(&[u32]),
727 ) -> Result<FleetGeneration, DaemonError> {
728 let mut delivered = false;
729 let first = self.generate_streaming_once(session_id, max_new_tokens, max_wall_ms, cancel, opts, on_logprobs, |t| {
730 delivered = true;
731 on_tokens(t)
732 });
733 match first {
734 Err(e) if !delivered && is_transient(&e) => {
737 self.reconnect_self()?;
738 self.generate_streaming_once(session_id, max_new_tokens, max_wall_ms, cancel, opts, on_logprobs, on_tokens)
739 }
740 r => r,
741 }
742 }
743
744 fn needs_v3(&self, what: &str) -> Result<(), DaemonError> {
746 if self.endpoint.version() >= 3 {
747 return Ok(());
748 }
749 Err(DaemonError::Unsupported(crate::Refusal::new(
750 "unsupported_on_fleet",
751 None,
752 format!("node {} runs an older superfluid that cannot take {what}; update it", self.node.node_identity),
753 )))
754 }
755
756 pub fn tokenizer_export(&mut self) -> Result<linkf::TokenizerExportMsg, DaemonError> {
758 self.needs_v3("sharing its tokenizer")?;
759 let request_id = self.next_generation;
760 self.next_generation += 1;
761 self.endpoint.send(msg_type::TOKENIZER_EXPORT_REQ, &linkf::TokenizerExportReqMsg { request_id })?;
762 loop {
763 for frame in self.endpoint.wait_frames()? {
764 let Some(frame) = self.absorb(frame)?.filter(|f| f.msg_type == msg_type::TOKENIZER_EXPORT) else {
765 continue;
766 };
767 let m: linkf::TokenizerExportMsg = postcard::from_bytes(&frame.payload)?;
768 if m.request_id == request_id {
769 return Ok(m);
770 }
771 }
772 }
773 }
774
775 pub fn tokenize(&mut self, text: linkf::TokenizeMsg) -> Result<Option<Vec<u32>>, DaemonError> {
777 self.needs_v3("encoding text")?;
778 let request_id = self.next_generation;
779 self.next_generation += 1;
780 self.endpoint.send(msg_type::TOKENIZE_REQ, &linkf::TokenizeReqMsg { request_id, text })?;
781 loop {
782 for frame in self.endpoint.wait_frames()? {
783 let Some(frame) = self.absorb(frame)?.filter(|f| f.msg_type == msg_type::TOKENIZE_RESP) else {
784 continue;
785 };
786 let m: linkf::TokenizeRespMsg = postcard::from_bytes(&frame.payload)?;
787 if m.request_id == request_id {
788 return Ok(m.tokens);
789 }
790 }
791 }
792 }
793
794 pub fn state_sync(
796 &mut self,
797 req: superfluid_proto::linkw::StateSyncReqMsg,
798 blobs: &[Blob],
799 ) -> Result<superfluid_proto::linkw::StateSyncOkMsg, DaemonError> {
800 self.state_sync_streaming(req, blobs, &mut |_| {})
801 }
802
803 pub fn state_sync_streaming(
806 &mut self,
807 req: superfluid_proto::linkw::StateSyncReqMsg,
808 blobs: &[Blob],
809 on_segment: &mut dyn FnMut(superfluid_proto::linkw::TranscribeSegmentMsg),
810 ) -> Result<superfluid_proto::linkw::StateSyncOkMsg, DaemonError> {
811 self.needs_v3("this operation")?;
812 self.send_blobs(blobs)?;
813 let request_id = self.next_generation;
814 self.next_generation += 1;
815 self.endpoint.send(msg_type::STATE_SYNC_REQ, &linkf::StateSyncReqF { request_id, req })?;
816 loop {
817 for frame in self.endpoint.wait_frames()? {
818 let Some(frame) = self.absorb(frame)? else { continue };
819 if frame.msg_type == msg_type::STATE_SYNC_SEGMENT {
820 let m: linkf::StateSyncSegmentF = postcard::from_bytes(&frame.payload)?;
821 if m.request_id == request_id {
822 on_segment(m.segment);
823 }
824 continue;
825 }
826 if frame.msg_type != msg_type::STATE_SYNC_RESP {
827 continue;
828 }
829 let m: linkf::StateSyncRespF = postcard::from_bytes(&frame.payload)?;
830 if m.request_id == request_id {
831 return m
832 .result
833 .map_err(|e| DaemonError::Generation(format!("node {}: {e}", self.node.node_identity)));
834 }
835 }
836 }
837 }
838
839 #[allow(clippy::too_many_arguments)]
840 fn generate_streaming_once(
841 &mut self,
842 session_id: u64,
843 max_new_tokens: u64,
844 max_wall_ms: u64,
845 cancel: &std::sync::atomic::AtomicBool,
846 opts: Option<&Ask<'_>>,
847 on_logprobs: &mut dyn FnMut(&[linkf::TokenLogprobMsg]),
848 mut on_tokens: impl FnMut(&[u32]),
849 ) -> Result<FleetGeneration, DaemonError> {
850 if opts.is_some() {
851 self.needs_v3("these generation options")?;
852 }
853 let generation_id = self.next_generation;
854 self.next_generation += 1;
855 let req = self
856 .head
857 .generate(
858 session_id,
859 generation_id,
860 GenerationBudgetsMsg { max_new_tokens, max_wall_ms },
861 )
862 .ok_or(DaemonError::Protocol("generate before assign"))?;
863 if let Some(ask) = opts {
864 self.send_blobs(ask.blobs)?;
865 let o = linkf::GenerateOptsMsg { session_id, generation_id, ..ask.opts.clone() };
866 self.endpoint.send(msg_type::GENERATE_OPTS, &o)?;
867 }
868 self.endpoint.send(msg_type::GENERATE_REQ, &req)?;
869
870 let mut tokens = Vec::new();
871 let mut finish = 0u32;
872 let mut done = false;
873 let mut cancel_sent = false;
874 let deadline = self.wall_deadline(max_wall_ms);
875 while !done {
876 if deadline.is_some_and(|d| std::time::Instant::now() >= d) {
877 return Err(superfluid_linkf::LinkFError::TimedOut.into());
878 }
879 if !cancel_sent && cancel.load(std::sync::atomic::Ordering::Relaxed) {
880 self.endpoint.send(
881 msg_type::GENERATE_CANCEL,
882 &GenerateCancelMsg { session_id, generation_id },
883 )?;
884 cancel_sent = true;
885 }
886 for frame in self.endpoint.pump()? {
889 let Some(frame) = self.absorb(frame)?.filter(|f| f.msg_type == msg_type::TOKEN_EVENTS) else {
890 continue;
891 };
892 let m: linkf::TokenEventsMsg = postcard::from_bytes(&frame.payload)?;
893 for ev in &m.events {
894 if ev.session_id == session_id && ev.generation_id == generation_id {
895 match &ev.payload {
896 TokenEventPayload::Tokens(t) => {
897 tokens.extend_from_slice(t);
898 on_tokens(t);
899 }
900 TokenEventPayload::Logprobs(items) => on_logprobs(items),
901 TokenEventPayload::Finish { reason } => {
902 finish = *reason;
903 done = true;
904 }
905 _ => {}
906 }
907 }
908 }
909 for ack in self.head.on_token_events(&m) {
910 self.endpoint.send(msg_type::WATERMARK_ACK, &ack)?;
911 }
912 }
913 }
914 let expired = finish == u32::MAX;
915 self.head.retire_generation(session_id, generation_id);
916 Ok(FleetGeneration { tokens, finish, expired })
917 }
918
919 pub fn forward_stage(
920 &mut self,
921 tokens: &[u32],
922 start_layer: u32,
923 end_layer: u32,
924 hidden_in: Option<Vec<u8>>,
925 ) -> Result<superfluid_engine::StageOutput, DaemonError> {
926 let request_id = self.next_generation;
927 self.next_generation += 1;
928 let req = linkf::StageForwardReqMsg {
929 request_id,
930 tokens: tokens.to_vec(),
931 start_layer,
932 end_layer,
933 hidden_in,
934 };
935 self.endpoint.send(msg_type::STAGE_FORWARD_REQ, &req)?;
936 loop {
937 for frame in self.endpoint.wait_frames()? {
938 let Some(frame) = self.absorb(frame)? else { continue };
939 if frame.msg_type == msg_type::STAGE_FORWARD_RESP {
940 let m: linkf::StageForwardRespMsg = postcard::from_bytes(&frame.payload)?;
941 if m.request_id != request_id {
942 continue;
943 }
944 return match (m.hidden, m.token) {
945 (Some(h), None) => Ok(superfluid_engine::StageOutput::Hidden(h)),
946 (None, Some(t)) => Ok(superfluid_engine::StageOutput::Token(t)),
947 _ => Err(DaemonError::Generation(
948 "stage node could not run the forward (engine lacks staged forward)".into(),
949 )),
950 };
951 }
952 }
953 }
954 }
955
956 pub fn generate_chat(
957 &mut self,
958 session_id: u64,
959 codec: &dyn TextCodec,
960 messages: &[crate::codec::ChatMessage],
961 tools: &[String],
962 max_new_tokens: u64,
963 max_wall_ms: u64,
964 ) -> Result<Vec<FleetSegment>, DaemonError> {
965 let prompt = codec
966 .render_prompt_structured(messages, tools)
967 .ok_or(DaemonError::Protocol("codec has no chat dialect"))?;
968 self.append(session_id, &prompt)?;
969 let gen = self.generate(session_id, max_new_tokens, max_wall_ms)?;
970
971 let mut chan = crate::codec::primed_channelizer(codec, &prompt);
972 let schemas = crate::codec::ToolSchemas::from_jsons(tools);
973 let mut segments = Vec::new();
974 for run in chan.split(&gen.tokens) {
975 let text = codec.decode(&run.text);
976 let tool = if run.channel == crate::wal::channel::TOOL_CALL && run.closes {
977 codec.parse_tool_call_with(&text, Some(&schemas))
978 } else {
979 None
980 };
981 if !text.is_empty() || tool.is_some() {
982 segments.push(FleetSegment {
983 channel: run.channel,
984 text,
985 tool,
986 });
987 }
988 }
989 Ok(segments)
990 }
991
992 fn recovered_generation(
993 &self,
994 session_id: u64,
995 generation_id: u64,
996 ) -> Option<FleetGeneration> {
997 let epoch = self.head.current_epoch(session_id)?;
998 let mut events: Vec<_> = self
999 .head
1000 .wal()
1001 .session_events(session_id)
1002 .iter()
1003 .filter(|c| c.event.epoch == epoch && c.event.generation_id == generation_id)
1004 .collect();
1005 events.sort_by_key(|c| c.event.event_seq);
1006 let mut tokens = Vec::new();
1007 let mut finish = None;
1008 for c in events {
1009 match &c.event.payload {
1010 TokenEventPayload::Tokens(t) => tokens.extend_from_slice(t),
1011 TokenEventPayload::Finish { reason } => finish = Some(*reason),
1012 _ => {}
1013 }
1014 }
1015 let finish = finish?;
1016 Some(FleetGeneration {
1017 tokens,
1018 finish,
1019 expired: finish == crate::nodeagent::FINISH_EXPIRED,
1020 })
1021 }
1022
1023 fn generate_once(
1024 &mut self,
1025 session_id: u64,
1026 generation_id: u64,
1027 max_new_tokens: u64,
1028 max_wall_ms: u64,
1029 ) -> Result<FleetGeneration, DaemonError> {
1030 let req = self
1031 .head
1032 .generate(
1033 session_id,
1034 generation_id,
1035 GenerationBudgetsMsg {
1036 max_new_tokens,
1037 max_wall_ms,
1038 },
1039 )
1040 .ok_or(DaemonError::Protocol("generate before assign"))?;
1041 self.endpoint.send(msg_type::GENERATE_REQ, &req)?;
1042
1043 let mut tokens = Vec::new();
1044 let mut finish = 0u32;
1045 let mut expired = false;
1046 let mut done = false;
1047 let deadline = self.wall_deadline(max_wall_ms);
1048 while !done {
1049 if deadline.is_some_and(|d| std::time::Instant::now() >= d) {
1050 return Err(superfluid_linkf::LinkFError::TimedOut.into());
1051 }
1052 for frame in self.endpoint.pump()? {
1055 let Some(frame) = self.absorb(frame)?.filter(|f| f.msg_type == msg_type::TOKEN_EVENTS) else {
1056 continue;
1057 };
1058 let m: linkf::TokenEventsMsg = postcard::from_bytes(&frame.payload)?;
1059 for ev in &m.events {
1060 if ev.session_id == session_id && ev.generation_id == generation_id {
1061 match &ev.payload {
1062 TokenEventPayload::Tokens(t) => tokens.extend_from_slice(t),
1063 TokenEventPayload::Finish { reason } => {
1064 finish = *reason;
1065 done = true;
1066 }
1067 _ => {}
1068 }
1069 }
1070 }
1071 for ack in self.head.on_token_events(&m) {
1072 self.endpoint.send(msg_type::WATERMARK_ACK, &ack)?;
1073 }
1074 }
1075 }
1076 if finish == u32::MAX {
1077 expired = true;
1078 }
1079 self.head.retire_generation(session_id, generation_id);
1080 Ok(FleetGeneration {
1081 tokens,
1082 finish,
1083 expired,
1084 })
1085 }
1086
1087 pub fn wal(&self) -> &superfluid_linkf::WalStub {
1088 self.head.wal()
1089 }
1090}
1091
1092pub struct PipelineFleet {
1093 stages: Vec<FleetHead>,
1094 boundaries: Vec<u32>,
1095}
1096
1097impl PipelineFleet {
1098 pub fn connect(nodes: Vec<(String, Vec<u8>)>, n_layers: u32) -> Result<PipelineFleet, DaemonError> {
1099 if nodes.is_empty() {
1100 return Err(DaemonError::Config("a pipeline needs at least one stage node"));
1101 }
1102 let p = nodes.len() as u32;
1103 let boundaries: Vec<u32> = (0..p).map(|i| ((i + 1) * n_layers) / p).collect();
1104 let mut stages = Vec::with_capacity(nodes.len());
1105 for (addr, auth) in nodes {
1106 stages.push(FleetHead::connect(&addr, auth)?);
1107 }
1108 Ok(PipelineFleet { stages, boundaries })
1109 }
1110
1111 pub fn boundaries(&self) -> &[u32] {
1112 &self.boundaries
1113 }
1114
1115 fn forward_one(&mut self, context: &[u32]) -> Result<u32, DaemonError> {
1116 let mut hidden: Option<Vec<u8>> = None;
1117 let mut start = 0u32;
1118 let n = self.stages.len();
1119 for i in 0..n {
1120 let end = self.boundaries[i];
1121 let empty: Vec<u32> = Vec::new();
1122 let tokens: &[u32] = if i == 0 { context } else { &empty };
1123 match self.stages[i].forward_stage(tokens, start, end, hidden.take())? {
1124 StageOutput::Hidden(h) => hidden = Some(h),
1125 StageOutput::Token(t) => return Ok(t),
1126 }
1127 start = end;
1128 }
1129 Err(DaemonError::Protocol(
1130 "pipeline finished every stage without a token — last stage must reach n_layers",
1131 ))
1132 }
1133
1134 pub fn generate(&mut self, context: &[u32], max_tokens: u32) -> Result<Vec<u32>, DaemonError> {
1135 let mut ctx = context.to_vec();
1136 let mut out = Vec::with_capacity(max_tokens as usize);
1137 for _ in 0..max_tokens {
1138 let tok = self.forward_one(&ctx)?;
1139 out.push(tok);
1140 ctx.push(tok);
1141 }
1142 Ok(out)
1143 }
1144}
1145
1146#[cfg(test)]
1147mod tests {
1148 use super::{Encoded, ENCODED_TOKENS};
1149
1150 #[test]
1151 fn text_encoded_before_is_answered_and_the_least_used_goes_first() {
1152 let mut enc = Encoded::default();
1153 let (a, b, c) = ([1u8; 32], [2u8; 32], [3u8; 32]);
1154 enc.put(a, vec![7; 10]);
1155 assert_eq!(enc.get(&a), Some(vec![7; 10]));
1156 assert_eq!(enc.get(&b), None);
1157 enc.put(a, vec![8; 4]);
1158 assert_eq!((enc.get(&a), enc.tokens), (Some(vec![8; 4]), 4), "the same text again replaces it");
1159
1160 enc.put(b, vec![0; ENCODED_TOKENS / 2]);
1161 assert!(enc.get(&a).is_some(), "a is used after b");
1162 enc.put(c, vec![0; ENCODED_TOKENS / 2]);
1163 assert_eq!((enc.get(&b), enc.get(&a)), (None, None));
1165 assert!(enc.get(&c).is_some(), "the newest stays");
1166 assert_eq!(enc.tokens, ENCODED_TOKENS / 2);
1167 }
1168}