1use std::collections::BTreeMap;
4
5use superfluid_linkf::{Endpoint, HeadSession};
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 struct FleetHead {
56 head: HeadSession,
57 endpoint: Endpoint,
58 node: HelloAckF,
59 next_generation: u64,
60 placements: BTreeMap<u64, Placement>,
61 addr: String,
62 auth: Vec<u8>,
63}
64
65static NEXT_EPOCH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
68static NEXT_COMMANDS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
69
70fn fresh_session_state() -> HeadSession {
71 HeadSession::starting_at(NEXT_COMMANDS.fetch_add(1 << 32, std::sync::atomic::Ordering::Relaxed))
72}
73
74pub const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
75
76pub const HANDSHAKE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
77
78fn closed_at_handshake() -> String {
79 "closed the connection at the handshake; check that --fleet-auth holds the token in the node's --auth-file".into()
80}
81
82fn handshake(addr: &str, auth: Vec<u8>) -> Result<(Endpoint, HelloAckF), DaemonError> {
84 let node = |why: String| DaemonError::FleetNode { addr: addr.to_string(), why };
85 let stream = Endpoint::tcp_connect_within(addr, CONNECT_TIMEOUT).map_err(|e| node(format!("unreachable ({e})")))?;
86 let opened = if auth.is_empty() {
87 Endpoint::from_tcp(stream)
88 } else {
89 Endpoint::from_tcp_secure(stream, &auth, superfluid_linkf::Role::Initiator, HANDSHAKE_TIMEOUT)
90 };
91 hello(opened, addr, auth, "--fleet-auth and the node's --auth-file differ")
92}
93
94fn hello(opened: Result<Endpoint, superfluid_linkf::LinkFError>, peer: &str, auth: Vec<u8>, mismatch: &str) -> Result<(Endpoint, HelloAckF), DaemonError> {
98 use superfluid_linkf::LinkFError;
99 let node = |why: String| DaemonError::FleetNode { addr: peer.to_string(), why };
100 let mut endpoint = match opened {
101 Ok(ep) => ep,
102 Err(LinkFError::Unauthorized) => return Err(node(format!("refused the fleet token: {mismatch}"))),
103 Err(LinkFError::TimedOut) => {
104 return Err(node(format!("did not finish the handshake within {} s", HANDSHAKE_TIMEOUT.as_secs())))
105 }
106 Err(LinkFError::Closed | LinkFError::Io(_)) if !auth.is_empty() => return Err(node(closed_at_handshake())),
107 Err(e) => return Err(node(e.to_string())),
108 };
109 match endpoint.hello_as_head_within(auth, HANDSHAKE_TIMEOUT) {
110 Ok(ack) => Ok((endpoint, ack)),
111 Err(LinkFError::Unauthorized) => Err(node(format!("refused the fleet token: {mismatch}"))),
112 Err(LinkFError::Closed | LinkFError::Io(_)) => Err(node(closed_at_handshake())),
113 Err(LinkFError::TimedOut) => Err(node(format!(
114 "did not answer the handshake within {} s; is superfluid-noded listening there?",
115 HANDSHAKE_TIMEOUT.as_secs()
116 ))),
117 Err(e) => Err(node(e.to_string())),
118 }
119}
120
121fn is_transient(e: &DaemonError) -> bool {
122 matches!(
123 e,
124 DaemonError::Fleet(superfluid_linkf::LinkFError::Closed)
125 | DaemonError::Fleet(superfluid_linkf::LinkFError::TimedOut)
126 | DaemonError::Fleet(superfluid_linkf::LinkFError::Io(_))
127 )
128}
129
130impl FleetHead {
131 pub fn connect(addr: &str, auth: Vec<u8>) -> Result<FleetHead, DaemonError> {
132 let (endpoint, node) = handshake(addr, auth.clone())?;
133 Ok(FleetHead::from_parts(endpoint, node, addr.to_string(), auth))
134 }
135
136 pub fn accept(sock: std::net::TcpStream, auth: Vec<u8>) -> Result<FleetHead, DaemonError> {
138 let peer = sock.peer_addr().map(|a| a.to_string()).unwrap_or_default();
139 let opened = if auth.is_empty() {
140 Endpoint::from_tcp(sock)
141 } else {
142 Endpoint::from_tcp_secure(sock, &auth, superfluid_linkf::Role::Initiator, HANDSHAKE_TIMEOUT)
143 };
144 let (endpoint, node) = hello(opened, &peer, auth.clone(), "its token differs from this head's")?;
145 Ok(FleetHead::from_parts(endpoint, node, String::new(), auth))
146 }
147
148 fn from_parts(endpoint: Endpoint, node: HelloAckF, addr: String, auth: Vec<u8>) -> FleetHead {
149 FleetHead {
150 head: fresh_session_state(),
151 endpoint,
152 node,
153 next_generation: 1,
154 placements: BTreeMap::new(),
155 addr,
156 auth,
157 }
158 }
159
160 fn reconnect_self(&mut self) -> Result<(), DaemonError> {
161 let (addr, auth) = (self.addr.clone(), self.auth.clone());
162 self.reconnect(&addr, auth)?;
163 Ok(())
164 }
165
166 pub fn node(&self) -> &HelloAckF {
167 &self.node
168 }
169
170 pub fn drain_status(&mut self) -> Result<(), DaemonError> {
172 for frame in self.endpoint.pump()? {
173 if frame.msg_type == msg_type::NODE_STATUS {
174 if let Ok(m) = postcard::from_bytes::<linkf::NodeStatusMsg>(&frame.payload) {
175 self.node.capacity = m.capacity;
176 if !m.loadable_models.is_empty() {
177 self.node.loadable_models = m.loadable_models;
178 }
179 }
180 }
181 }
182 Ok(())
183 }
184
185 pub fn keepalive(&mut self) -> Result<(), DaemonError> {
186 self.endpoint.send(msg_type::KEEPALIVE, &())?;
187 self.drain_status()
188 }
189
190 pub fn knows(&self, session_id: u64) -> bool {
191 self.head.current_epoch(session_id).is_some()
192 }
193
194
195 pub fn reconnect(&mut self, addr: &str, auth: Vec<u8>) -> Result<Vec<u64>, DaemonError> {
196 let _ = self.endpoint.shutdown();
197 if addr.is_empty() {
198 return Err(DaemonError::FleetNode {
199 addr: self.node.node_identity.clone(),
200 why: "dialed in on its own; waiting for it to join again".into(),
201 });
202 }
203 let (endpoint, node) = handshake(addr, auth)?;
204 self.node = node;
205 self.endpoint = endpoint;
206 let rec = self.head.reconcile();
207 self.endpoint.send(msg_type::RECONCILE, &rec)?;
208
209 let mut received: std::collections::BTreeSet<(u64, u64, u64)> =
210 std::collections::BTreeSet::new();
211 let ack = loop {
212 let mut got = None;
213 for frame in self.endpoint.wait_frames()? {
214 match frame.msg_type {
215 msg_type::RECONCILE_ACK => {
216 got = Some(postcard::from_bytes::<linkf::ReconcileAckMsg>(&frame.payload)?);
217 }
218 msg_type::TOKEN_EVENTS => {
219 let m: linkf::TokenEventsMsg = postcard::from_bytes(&frame.payload)?;
220 for ev in &m.events {
221 received.insert((ev.session_id, ev.generation_id, ev.event_seq));
222 }
223 for a in self.head.on_token_events(&m) {
224 self.endpoint.send(msg_type::WATERMARK_ACK, &a)?;
225 }
226 }
227 _ => {}
228 }
229 }
230 if let Some(a) = got {
231 break a;
232 }
233 };
234
235 let mut expected: std::collections::BTreeSet<(u64, u64, u64)> =
236 std::collections::BTreeSet::new();
237 for s in &ack.sessions {
238 if self.head.current_epoch(s.session_id).is_some() {
239 for r in &s.retry_ranges {
240 for seq in r.from_seq..=r.to_seq {
241 let id = (s.session_id, r.generation_id, seq);
242 if !received.contains(&id) {
243 expected.insert(id);
244 }
245 }
246 }
247 }
248 }
249 while !expected.is_empty() {
250 for frame in self.endpoint.wait_frames()? {
251 if frame.msg_type != msg_type::TOKEN_EVENTS {
252 continue;
253 }
254 let m: linkf::TokenEventsMsg = postcard::from_bytes(&frame.payload)?;
255 for ev in &m.events {
256 expected.remove(&(ev.session_id, ev.generation_id, ev.event_seq));
257 }
258 for a in self.head.on_token_events(&m) {
259 self.endpoint.send(msg_type::WATERMARK_ACK, &a)?;
260 }
261 }
262 }
263
264 let reassigned = self.head.on_reconcile_ack(&ack);
267 for s in ack.sessions.iter().filter(|s| s.held && !reassigned.contains(&s.session_id)) {
268 if let (Some(p), Some(epoch)) =
269 (self.placements.get(&s.session_id).cloned(), self.head.current_epoch(s.session_id))
270 {
271 if !p.tokens.is_empty() {
272 self.send_append(s.session_id, epoch, &p.tokens)?;
273 }
274 }
275 }
276 let mut replaced = Vec::new();
277 for session_id in reassigned {
278 if let Some(p) = self.placements.get(&session_id).cloned() {
279 if let Some(epoch) = self.head.current_epoch(session_id) {
280 let mut msg = self.head.assign_session(
281 session_id,
282 epoch,
283 p.lease,
284 p.qos_class,
285 &p.codec,
286 );
287 msg.params = encode_params(&p.params);
288 self.endpoint.send(msg_type::SESSION_ASSIGN, &msg)?;
289 if !p.tokens.is_empty() {
290 self.send_append(session_id, epoch, &p.tokens)?;
291 }
292 replaced.push(session_id);
293 }
294 }
295 }
296 Ok(replaced)
297 }
298
299 pub fn assign(
300 &mut self,
301 session_id: u64,
302 params: GenParams,
303 qos_class: u8,
304 codec: &str,
305 lease: Lease,
306 ) -> Result<u64, DaemonError> {
307 let entry = self.placements.entry(session_id).or_insert(Placement {
308 params,
309 qos_class,
310 codec: codec.to_string(),
311 lease,
312 tokens: Vec::new(),
313 });
314 entry.params = params;
315 entry.qos_class = qos_class;
316 entry.codec = codec.to_string();
317 entry.lease = lease;
318
319 let epoch = NEXT_EPOCH.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
320 let mut msg = self.head.assign_session(session_id, epoch, lease, qos_class, codec);
321 msg.params = encode_params(¶ms);
322 match self.endpoint.send(msg_type::SESSION_ASSIGN, &msg) {
323 Ok(()) => Ok(epoch),
324 Err(e) => {
325 let e: DaemonError = e.into();
326 if is_transient(&e) {
327 self.reconnect_self()?;
328 Ok(self.head.current_epoch(session_id).unwrap_or(epoch))
329 } else {
330 Err(e)
331 }
332 }
333 }
334 }
335
336 pub fn park(&mut self, session_id: u64) -> Result<(), DaemonError> {
337 let epoch = self.head.current_epoch(session_id).unwrap_or(0);
338 self.revoke(session_id, epoch)
339 }
340
341 pub fn revoke(&mut self, session_id: u64, epoch: u64) -> Result<(), DaemonError> {
344 self.placements.remove(&session_id);
345 let msg = match self.head.revoke(session_id) {
346 Some(msg) => msg,
347 None => linkf::SessionRevokeMsg { command_id: self.head.next_command(session_id), session_id, epoch },
348 };
349 self.endpoint.send(msg_type::SESSION_REVOKE, &msg)?;
350 Ok(())
351 }
352
353 pub fn append(&mut self, session_id: u64, tokens: &[u32]) -> Result<(), DaemonError> {
354 let epoch = self
355 .head
356 .current_epoch(session_id)
357 .ok_or(DaemonError::Protocol("append before assign"))?;
358 if let Some(p) = self.placements.get_mut(&session_id) {
359 p.tokens = tokens.to_vec();
360 }
361 match self.send_append(session_id, epoch, tokens) {
362 Ok(()) => Ok(()),
363 Err(e) if is_transient(&e) => {
364 self.reconnect_self()
365 }
366 Err(e) => Err(e),
367 }
368 }
369
370 fn send_append(&mut self, session_id: u64, epoch: u64, tokens: &[u32]) -> Result<(), DaemonError> {
371 let msg = AppendContentMsg {
372 command_id: self.head.next_command(session_id),
373 session_id,
374 epoch,
375 blocks: encode_tokens(tokens),
376 spans: Vec::new(),
377 media_refs: Vec::new(),
378 };
379 self.endpoint.send(msg_type::APPEND_CONTENT, &msg)?;
380 Ok(())
381 }
382
383 pub fn generate(
384 &mut self,
385 session_id: u64,
386 max_new_tokens: u64,
387 max_wall_ms: u64,
388 ) -> Result<FleetGeneration, DaemonError> {
389 let generation_id = self.next_generation;
390 self.next_generation += 1;
391 for attempt in 0..2 {
392 match self.generate_once(session_id, generation_id, max_new_tokens, max_wall_ms) {
393 Ok(g) => return Ok(g),
394 Err(e) if attempt == 0 && is_transient(&e) => {
395 self.reconnect_self()?;
396 if let Some(g) = self.recovered_generation(session_id, generation_id) {
397 self.head.retire_generation(session_id, generation_id);
398 return Ok(g);
399 }
400 continue;
401 }
402 Err(e) => return Err(e),
403 }
404 }
405 unreachable!("generate retried at most once")
406 }
407
408 pub fn generate_streaming(
409 &mut self,
410 session_id: u64,
411 max_new_tokens: u64,
412 max_wall_ms: u64,
413 cancel: &std::sync::atomic::AtomicBool,
414 mut on_tokens: impl FnMut(&[u32]),
415 ) -> Result<FleetGeneration, DaemonError> {
416 let mut delivered = false;
417 let first = self.generate_streaming_once(session_id, max_new_tokens, max_wall_ms, cancel, |t| {
418 delivered = true;
419 on_tokens(t)
420 });
421 match first {
422 Err(e) if !delivered && is_transient(&e) => {
425 self.reconnect_self()?;
426 self.generate_streaming_once(session_id, max_new_tokens, max_wall_ms, cancel, on_tokens)
427 }
428 r => r,
429 }
430 }
431
432 fn generate_streaming_once(
433 &mut self,
434 session_id: u64,
435 max_new_tokens: u64,
436 max_wall_ms: u64,
437 cancel: &std::sync::atomic::AtomicBool,
438 mut on_tokens: impl FnMut(&[u32]),
439 ) -> Result<FleetGeneration, DaemonError> {
440 let generation_id = self.next_generation;
441 self.next_generation += 1;
442 let req = self
443 .head
444 .generate(
445 session_id,
446 generation_id,
447 GenerationBudgetsMsg { max_new_tokens, max_wall_ms },
448 )
449 .ok_or(DaemonError::Protocol("generate before assign"))?;
450 self.endpoint.send(msg_type::GENERATE_REQ, &req)?;
451
452 let mut tokens = Vec::new();
453 let mut finish = 0u32;
454 let mut done = false;
455 let mut cancel_sent = false;
456 while !done {
457 if !cancel_sent && cancel.load(std::sync::atomic::Ordering::Relaxed) {
458 self.endpoint.send(
459 msg_type::GENERATE_CANCEL,
460 &GenerateCancelMsg { session_id, generation_id },
461 )?;
462 cancel_sent = true;
463 }
464 for frame in self.endpoint.wait_frames()? {
465 match frame.msg_type {
466 msg_type::TOKEN_EVENTS => {
467 let m: linkf::TokenEventsMsg = postcard::from_bytes(&frame.payload)?;
468 for ev in &m.events {
469 if ev.session_id == session_id && ev.generation_id == generation_id {
470 match &ev.payload {
471 TokenEventPayload::Tokens(t) => {
472 tokens.extend_from_slice(t);
473 on_tokens(t);
474 }
475 TokenEventPayload::Finish { reason } => {
476 finish = *reason;
477 done = true;
478 }
479 _ => {}
480 }
481 }
482 }
483 for ack in self.head.on_token_events(&m) {
484 self.endpoint.send(msg_type::WATERMARK_ACK, &ack)?;
485 }
486 }
487 msg_type::LEASE_RENEW => {
488 let m: linkf::LeaseRenewMsg = postcard::from_bytes(&frame.payload)?;
489 if let Some(grant) = self.head.on_lease_renew(&m) {
490 self.endpoint.send(msg_type::LEASE_GRANT, &grant)?;
491 }
492 }
493 msg_type::NODE_STATUS => {
494 if let Ok(m) = postcard::from_bytes::<linkf::NodeStatusMsg>(&frame.payload)
495 {
496 self.node.capacity = m.capacity;
497 if !m.loadable_models.is_empty() {
498 self.node.loadable_models = m.loadable_models;
499 }
500 }
501 }
502 _ => {}
503 }
504 }
505 }
506 let expired = finish == u32::MAX;
507 self.head.retire_generation(session_id, generation_id);
508 Ok(FleetGeneration { tokens, finish, expired })
509 }
510
511 pub fn forward_stage(
512 &mut self,
513 tokens: &[u32],
514 start_layer: u32,
515 end_layer: u32,
516 hidden_in: Option<Vec<u8>>,
517 ) -> Result<superfluid_engine::StageOutput, DaemonError> {
518 let request_id = self.next_generation;
519 self.next_generation += 1;
520 let req = linkf::StageForwardReqMsg {
521 request_id,
522 tokens: tokens.to_vec(),
523 start_layer,
524 end_layer,
525 hidden_in,
526 };
527 self.endpoint.send(msg_type::STAGE_FORWARD_REQ, &req)?;
528 loop {
529 for frame in self.endpoint.wait_frames()? {
530 if frame.msg_type == msg_type::STAGE_FORWARD_RESP {
531 let m: linkf::StageForwardRespMsg = postcard::from_bytes(&frame.payload)?;
532 if m.request_id != request_id {
533 continue;
534 }
535 return match (m.hidden, m.token) {
536 (Some(h), None) => Ok(superfluid_engine::StageOutput::Hidden(h)),
537 (None, Some(t)) => Ok(superfluid_engine::StageOutput::Token(t)),
538 _ => Err(DaemonError::Generation(
539 "stage node could not run the forward (engine lacks staged forward)".into(),
540 )),
541 };
542 }
543 }
544 }
545 }
546
547 pub fn generate_chat(
548 &mut self,
549 session_id: u64,
550 codec: &dyn TextCodec,
551 messages: &[crate::codec::ChatMessage],
552 tools: &[String],
553 max_new_tokens: u64,
554 max_wall_ms: u64,
555 ) -> Result<Vec<FleetSegment>, DaemonError> {
556 let prompt = codec
557 .render_prompt_structured(messages, tools)
558 .ok_or(DaemonError::Protocol("codec has no chat dialect"))?;
559 self.append(session_id, &prompt)?;
560 let gen = self.generate(session_id, max_new_tokens, max_wall_ms)?;
561
562 let mut chan = crate::codec::primed_channelizer(codec, &prompt);
563 let schemas = crate::codec::ToolSchemas::from_jsons(tools);
564 let mut segments = Vec::new();
565 for run in chan.split(&gen.tokens) {
566 let text = codec.decode(&run.text);
567 let tool = if run.channel == crate::wal::channel::TOOL_CALL && run.closes {
568 codec.parse_tool_call_with(&text, Some(&schemas))
569 } else {
570 None
571 };
572 if !text.is_empty() || tool.is_some() {
573 segments.push(FleetSegment {
574 channel: run.channel,
575 text,
576 tool,
577 });
578 }
579 }
580 Ok(segments)
581 }
582
583 fn recovered_generation(
584 &self,
585 session_id: u64,
586 generation_id: u64,
587 ) -> Option<FleetGeneration> {
588 let epoch = self.head.current_epoch(session_id)?;
589 let mut events: Vec<_> = self
590 .head
591 .wal()
592 .session_events(session_id)
593 .iter()
594 .filter(|c| c.event.epoch == epoch && c.event.generation_id == generation_id)
595 .collect();
596 events.sort_by_key(|c| c.event.event_seq);
597 let mut tokens = Vec::new();
598 let mut finish = None;
599 for c in events {
600 match &c.event.payload {
601 TokenEventPayload::Tokens(t) => tokens.extend_from_slice(t),
602 TokenEventPayload::Finish { reason } => finish = Some(*reason),
603 _ => {}
604 }
605 }
606 let finish = finish?;
607 Some(FleetGeneration {
608 tokens,
609 finish,
610 expired: finish == crate::nodeagent::FINISH_EXPIRED,
611 })
612 }
613
614 fn generate_once(
615 &mut self,
616 session_id: u64,
617 generation_id: u64,
618 max_new_tokens: u64,
619 max_wall_ms: u64,
620 ) -> Result<FleetGeneration, DaemonError> {
621 let req = self
622 .head
623 .generate(
624 session_id,
625 generation_id,
626 GenerationBudgetsMsg {
627 max_new_tokens,
628 max_wall_ms,
629 },
630 )
631 .ok_or(DaemonError::Protocol("generate before assign"))?;
632 self.endpoint.send(msg_type::GENERATE_REQ, &req)?;
633
634 let mut tokens = Vec::new();
635 let mut finish = 0u32;
636 let mut expired = false;
637 let mut done = false;
638 while !done {
639 for frame in self.endpoint.wait_frames()? {
640 match frame.msg_type {
641 msg_type::TOKEN_EVENTS => {
642 let m: linkf::TokenEventsMsg = postcard::from_bytes(&frame.payload)?;
643 for ev in &m.events {
644 if ev.session_id == session_id && ev.generation_id == generation_id {
645 match &ev.payload {
646 TokenEventPayload::Tokens(t) => tokens.extend_from_slice(t),
647 TokenEventPayload::Finish { reason } => {
648 finish = *reason;
649 done = true;
650 }
651 _ => {}
652 }
653 }
654 }
655 for ack in self.head.on_token_events(&m) {
656 self.endpoint.send(msg_type::WATERMARK_ACK, &ack)?;
657 }
658 }
659 msg_type::LEASE_RENEW => {
660 let m: linkf::LeaseRenewMsg = postcard::from_bytes(&frame.payload)?;
661 if let Some(grant) = self.head.on_lease_renew(&m) {
662 self.endpoint.send(msg_type::LEASE_GRANT, &grant)?;
663 }
664 }
665 msg_type::NODE_STATUS => {
666 if let Ok(m) = postcard::from_bytes::<linkf::NodeStatusMsg>(&frame.payload)
667 {
668 self.node.capacity = m.capacity;
669 if !m.loadable_models.is_empty() {
670 self.node.loadable_models = m.loadable_models;
671 }
672 }
673 }
674 _ => {}
675 }
676 }
677 }
678 if finish == u32::MAX {
679 expired = true;
680 }
681 self.head.retire_generation(session_id, generation_id);
682 Ok(FleetGeneration {
683 tokens,
684 finish,
685 expired,
686 })
687 }
688
689 pub fn wal(&self) -> &superfluid_linkf::WalStub {
690 self.head.wal()
691 }
692}
693
694pub struct PipelineFleet {
695 stages: Vec<FleetHead>,
696 boundaries: Vec<u32>,
697}
698
699impl PipelineFleet {
700 pub fn connect(nodes: Vec<(String, Vec<u8>)>, n_layers: u32) -> Result<PipelineFleet, DaemonError> {
701 if nodes.is_empty() {
702 return Err(DaemonError::Config("a pipeline needs at least one stage node"));
703 }
704 let p = nodes.len() as u32;
705 let boundaries: Vec<u32> = (0..p).map(|i| ((i + 1) * n_layers) / p).collect();
706 let mut stages = Vec::with_capacity(nodes.len());
707 for (addr, auth) in nodes {
708 stages.push(FleetHead::connect(&addr, auth)?);
709 }
710 Ok(PipelineFleet { stages, boundaries })
711 }
712
713 pub fn boundaries(&self) -> &[u32] {
714 &self.boundaries
715 }
716
717 fn forward_one(&mut self, context: &[u32]) -> Result<u32, DaemonError> {
718 let mut hidden: Option<Vec<u8>> = None;
719 let mut start = 0u32;
720 let n = self.stages.len();
721 for i in 0..n {
722 let end = self.boundaries[i];
723 let empty: Vec<u32> = Vec::new();
724 let tokens: &[u32] = if i == 0 { context } else { &empty };
725 match self.stages[i].forward_stage(tokens, start, end, hidden.take())? {
726 StageOutput::Hidden(h) => hidden = Some(h),
727 StageOutput::Token(t) => return Ok(t),
728 }
729 start = end;
730 }
731 Err(DaemonError::Protocol(
732 "pipeline finished every stage without a token — last stage must reach n_layers",
733 ))
734 }
735
736 pub fn generate(&mut self, context: &[u32], max_tokens: u32) -> Result<Vec<u32>, DaemonError> {
737 let mut ctx = context.to_vec();
738 let mut out = Vec::with_capacity(max_tokens as usize);
739 for _ in 0..max_tokens {
740 let tok = self.forward_one(&ctx)?;
741 out.push(tok);
742 ctx.push(tok);
743 }
744 Ok(out)
745 }
746}