Skip to main content

superfluid_daemon/
fleet.rs

1//! The head daemon's fleet edge.
2
3use 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
55/// What a tokenizer turns tokens into, as a digest: two tokenizers with one digest give every
56/// token id the same bytes.
57pub 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
71/// A tokenizer a head without the model takes from a node serving it: what it says without
72/// encoding is the node's own, kept here, and text is encoded by a node serving the model,
73/// once per request rather than once per token. Text it encoded before is answered here: a
74/// conversation sends its system prompt and earlier turns again with every turn.
75pub 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/// Encodings kept by the SHA-256 of what was encoded, up to [`ENCODED_TOKENS`] tokens, the
83/// least recently used going first.
84#[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    /// The node's tokenizer digest, which every node serving the model is held to.
129    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/// A file a request names, sent to the node by its SHA-256 (in hex) before the request.
197#[derive(Debug, Clone)]
198pub struct Blob {
199    pub hash: String,
200    pub path: std::path::PathBuf,
201}
202
203/// What a generation asks beyond its sampling: its options, and the blobs they name.
204pub struct Ask<'a> {
205    pub opts: &'a linkf::GenerateOptsMsg,
206    pub blobs: &'a [Blob],
207}
208
209/// A blob crosses in pieces this size, under the frame limit.
210const 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    /// The blobs this connection has sent, which the node keeps.
223    blobs_sent: std::collections::HashSet<String>,
224    /// The node's tokenizer digest (0: not known, as from a node on version 2).
225    tokenizer: u64,
226}
227
228/// Told each capacity a node reports, as the call that reads it, a generation included, reads it.
229pub type StatusHook = std::sync::Arc<dyn Fn(&superfluid_proto::handshake::CapacitySummary) + Send + Sync>;
230
231/// Epochs and command ids are numbered across every connection this head makes: a node keeps
232/// a session's across connections, and refuses an epoch or command it has already seen.
233static 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
248/// Dials `addr` and opens the link: encrypted and keyed by the token when there is one.
249fn 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
260/// The head's side of the handshake on a stream just opened to `peer`, dialed or accepted,
261/// with each way it can fail named for the operator; `mismatch` says what differs when the
262/// token is refused.
263fn 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    /// A node that dialed in; the head still opens the handshake, encrypted when it has a token.
305    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    /// The node's tokenizer digest, 0 when it did not say.
336    pub fn tokenizer(&self) -> u64 {
337        self.tokenizer
338    }
339
340    /// Reads the tokenizer digest a node on version 3 sends just after the handshake.
341    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    /// Sends each of `blobs` this connection has not sent yet.
361    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    /// How long the head waits on a peer that says nothing where its silence does not end the
388    /// wait (a version 2 node): the silence limit, or the default one when that is off.
389    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    /// When the head stops waiting on a generation: [`Self::grace`] past its own wall-clock
397    /// budget, as the node ends it at the budget, so one that has not by then never will.
398    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    /// The next frames the node sends, or `TimedOut` when none come `within`.
404    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    /// Keeps this connection, and the ones that replace it, alive as `liveness` says.
418    pub fn keep_alive(&mut self, liveness: Liveness) {
419        self.liveness = liveness;
420        self.endpoint.keep_alive(liveness);
421    }
422
423    /// Tells `hook` every status the node sends from now on, over this connection and the ones
424    /// that replace it.
425    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    /// Reads what the node sent while the connection sat idle, which also sends it a heartbeat
440    /// when one is due; an error means it is gone.
441    /// Asks a node that dialed in for a connection for short calls ([`msg_type::CONTROL_ASK`]).
442    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    /// Takes what any read of the link can carry, a status or a lease renewal, and hands back
455    /// the frames left for the caller.
456    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        // A session the node still holds may have missed the last append, written into the
561        // connection that dropped; resend each one's latest content.
562        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(&params);
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    /// Ends the session on the node, which frees the context it kept. `epoch` is the one it was
638    /// last assigned, so this also reaches a session placed over another connection.
639    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    /// As [`Self::generate_streaming`], with what the generation asks beyond its sampling, and
716    /// `on_logprobs` told the log-probabilities it asked for.
717    #[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            // A connection the node dropped while idle fails before anything streams; nothing
735            // reached the caller, so the generation can start over on a fresh one.
736            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    /// Refuses what a node on version 2 cannot be asked.
745    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    /// The node's tokenizer, whole but for encoding, for a head with no copy of the model.
757    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    /// Text the node's tokenizer encodes; none when it does not encode that way.
776    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    /// Runs one Link W state operation on the node's engine.
795    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    /// [`FleetHead::state_sync`], a streamed transcription's segments told to `on_segment` as
804    /// the node has them.
805    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            // One read at a time, not a wait for frames: a version 2 node is never given up for
887            // silence, so the deadline above is what ends a wait on one that went quiet.
888            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            // One read at a time, not a wait for frames: a version 2 node is never given up for
1053            // silence, so the deadline above is what ends a wait on one that went quiet.
1054            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        // Over the bound, the least recently used go until half of it is left.
1164        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}