Skip to main content

superfluid_daemon/
nodeagent.rs

1//! The node-agent.
2
3use std::collections::HashMap;
4use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
5use std::sync::{mpsc, Arc, Mutex};
6use std::time::{Duration, Instant};
7
8use superfluid_abi::finish;
9use superfluid_linkf::{AgentAction, AgentSession, Endpoint, LinkFError, Liveness};
10use superfluid_proto::handshake::{CapacitySummary, HelloAckF};
11use superfluid_proto::linkf::{self, msg_type, LinkFLimits, TokenEventPayload};
12
13use crate::fleet::{decode_params, decode_tokens};
14use crate::wal::GenParams;
15use crate::{Daemon, DaemonError};
16
17pub const FINISH_EXPIRED: u32 = u32::MAX;
18
19pub const FINISH_REFUSED: u32 = u32::MAX - 1;
20
21pub const FINISH_DECLINED: u32 = u32::MAX - 2;
22
23pub const DEFAULT_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
24
25/// How long the head may be silent on a connection before the node drops it: six of the
26/// head's heartbeats.
27pub const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
28
29struct AgentShared {
30    agent: AgentSession,
31    /// The connection each session was placed over, the one the head answers its renewals on.
32    owners: HashMap<u64, u64>,
33    contexts: HashMap<u64, Vec<u32>>,
34    /// Each session's sampling and the QoS class the head gave it, which its generations run in.
35    params: HashMap<u64, (GenParams, u8)>,
36    last_load_sample: Option<(u64, u64, u64)>,
37    decode_rate_ewma: u32,
38    prefill_rate_ewma: u32,
39    busy: Busy,
40}
41
42/// Time with at least one generation running, which the node's rates are measured over: a
43/// rate over wall time would read an idle node as a slow one.
44#[derive(Default)]
45struct Busy {
46    running: u32,
47    since: Option<Instant>,
48    total: Duration,
49}
50
51impl Busy {
52    fn start(&mut self) {
53        if self.running == 0 {
54            self.since = Some(Instant::now());
55        }
56        self.running += 1;
57    }
58
59    fn stop(&mut self) {
60        self.running = self.running.saturating_sub(1);
61        if self.running == 0 {
62            if let Some(t) = self.since.take() {
63                self.total += t.elapsed();
64            }
65        }
66    }
67
68    fn ms(&self) -> u64 {
69        (self.total + self.since.map(|t| t.elapsed()).unwrap_or_default()).as_millis() as u64
70    }
71}
72
73pub struct NodeAgent {
74    daemon: Arc<Daemon>,
75    shared: Arc<Mutex<AgentShared>>,
76    identity: String,
77    model_name: String,
78    start: Instant,
79    handshake_timeout: Duration,
80    idle_timeout: Duration,
81    auth: Vec<u8>,
82    max_lanes: u32,
83    last_status_ms: u64,
84    conn: u64,
85    /// When this connection last asked for each lease it has not had granted yet.
86    asked: HashMap<u64, Duration>,
87    /// What each generation the head is about to start asks beyond its sampling.
88    opts: HashMap<(u64, u64), linkf::GenerateOptsMsg>,
89    /// Blobs the head is sending: the SHA-256 each should have, and the bytes so far.
90    incoming: HashMap<u64, ([u8; 32], Vec<u8>)>,
91    /// The model's tokenizer digest, told to the head (0: not known).
92    tokenizer: u64,
93    /// The model's tokenizer, for a head that has no copy of the model to export and encode with.
94    tokenizer_impl: Option<Arc<dyn superfluid_engine::Tokenizer>>,
95    /// Called when a head this node dialed asks for a connection for short calls.
96    on_control_ask: Option<Arc<dyn Fn() + Send + Sync>>,
97}
98
99/// The largest blob a node takes from its head.
100const MAX_BLOB: u64 = 1 << 30;
101
102static NEXT_CONN: AtomicU64 = AtomicU64::new(1);
103
104const STATUS_INTERVAL_MS: u64 = 2_000;
105
106/// A renewal the head has not answered is asked for again after this long.
107const RENEW_RETRY: Duration = Duration::from_secs(1);
108
109/// How long one read of the link waits while a generation runs, which bounds how late its
110/// tokens leave.
111const POLL_RUNNING: Duration = Duration::from_millis(2);
112
113const POLL_IDLE: Duration = Duration::from_millis(20);
114
115/// Tokens are sent in batches of this many, or after [`EMIT_COALESCE_MS`]; a generation's first
116/// tokens go at once, so the head's first token waits on nothing.
117const EMIT_COALESCE_TOKENS: usize = 16;
118
119const EMIT_COALESCE_MS: u64 = 50;
120
121impl NodeAgent {
122    pub fn new(daemon: Arc<Daemon>, identity: impl Into<String>, model_name: impl Into<String>) -> NodeAgent {
123        NodeAgent {
124            max_lanes: 0,
125            last_status_ms: 0,
126            conn: NEXT_CONN.fetch_add(1, Ordering::Relaxed),
127            asked: HashMap::new(),
128            opts: HashMap::new(),
129            incoming: HashMap::new(),
130            tokenizer: 0,
131            tokenizer_impl: None,
132            on_control_ask: None,
133            daemon,
134            shared: Arc::new(Mutex::new(AgentShared {
135                agent: AgentSession::new(),
136                owners: HashMap::new(),
137                contexts: HashMap::new(),
138                params: HashMap::new(),
139                last_load_sample: None,
140                decode_rate_ewma: 0,
141                prefill_rate_ewma: 0,
142                busy: Busy::default(),
143            })),
144            identity: identity.into(),
145            model_name: model_name.into(),
146            start: Instant::now(),
147            handshake_timeout: DEFAULT_HANDSHAKE_TIMEOUT,
148            idle_timeout: DEFAULT_IDLE_TIMEOUT,
149            auth: Vec::new(),
150        }
151    }
152
153    pub fn with_max_lanes(mut self, lanes: u32) -> NodeAgent {
154        self.max_lanes = lanes;
155        self
156    }
157
158    pub fn connection(&self) -> NodeAgent {
159        NodeAgent {
160            daemon: Arc::clone(&self.daemon),
161            shared: Arc::clone(&self.shared),
162            identity: self.identity.clone(),
163            model_name: self.model_name.clone(),
164            start: self.start,
165            handshake_timeout: self.handshake_timeout,
166            idle_timeout: self.idle_timeout,
167            auth: self.auth.clone(),
168            max_lanes: self.max_lanes,
169            last_status_ms: 0,
170            conn: NEXT_CONN.fetch_add(1, Ordering::Relaxed),
171            asked: HashMap::new(),
172            opts: HashMap::new(),
173            incoming: HashMap::new(),
174            tokenizer: self.tokenizer,
175            tokenizer_impl: self.tokenizer_impl.clone(),
176            on_control_ask: self.on_control_ask.clone(),
177        }
178    }
179
180    /// `idle` is how long the head may be silent before a connection is dropped; zero keeps
181    /// every connection. Keep it well above the head's heartbeat, 5 s.
182    pub fn with_timeouts(mut self, handshake: Duration, idle: Duration) -> NodeAgent {
183        self.handshake_timeout = handshake;
184        self.idle_timeout = idle;
185        self
186    }
187
188    /// Tells each head the model's tokenizer by `digest` ([`crate::fleet::tokenizer_digest`]),
189    /// so one whose tokenizer differs places nothing here.
190    pub fn with_tokenizer(mut self, digest: u64) -> NodeAgent {
191        self.tokenizer = digest;
192        self
193    }
194
195    /// Serves the model's tokenizer to a head: its digest, all it says without encoding, and
196    /// the text it encodes, so a head with no copy of the model tokenizes as this node does.
197    pub fn with_tokenizer_of(mut self, tok: Arc<dyn superfluid_engine::Tokenizer>) -> NodeAgent {
198        self.tokenizer = crate::fleet::tokenizer_digest(&*tok);
199        self.tokenizer_impl = Some(tok);
200        self
201    }
202
203    /// For a node that dials its head: `dial` is called when the head asks for one connection
204    /// more, which it keeps for short calls; it may be called more than once.
205    pub fn with_on_control_ask(mut self, dial: impl Fn() + Send + Sync + 'static) -> NodeAgent {
206        self.on_control_ask = Some(Arc::new(dial));
207        self
208    }
209
210    pub fn with_auth(mut self, auth: Vec<u8>) -> NodeAgent {
211        self.auth = auth;
212        self
213    }
214
215    fn sample_load(&mut self) -> (u64, u64, u32, u32, u32, u32, u32, u64) {
216        let mut sh = self.shared.lock().expect("agent shared");
217        let stats = self.daemon.sched_stats();
218        let now = self.start.elapsed().as_millis() as u64;
219        let decoded = stats.decode_tokens.load(std::sync::atomic::Ordering::Relaxed);
220        let prefilled = stats.prefill_tokens.load(std::sync::atomic::Ordering::Relaxed);
221
222        let smooth = |prev: u32, inst: u32| -> u32 {
223            match prev {
224                0 => inst,
225                p => ((p as u64 * 3 + inst as u64) / 4) as u32,
226            }
227        };
228        const MIN_BUSY_MS: u64 = 200;
229        let busy = sh.busy.ms();
230        match sh.last_load_sample {
231            Some((prev_busy, prev_decoded, prev_prefilled)) => {
232                let dt_ms = busy.saturating_sub(prev_busy);
233                if dt_ms >= MIN_BUSY_MS {
234                    let rate = |n: u64, prev: u64| (n.saturating_sub(prev).saturating_mul(1000) / dt_ms) as u32;
235                    let (d, pf) = (rate(decoded, prev_decoded), rate(prefilled, prev_prefilled));
236                    if d > 0 {
237                        sh.decode_rate_ewma = smooth(sh.decode_rate_ewma, d);
238                    }
239                    if pf > 0 {
240                        sh.prefill_rate_ewma = smooth(sh.prefill_rate_ewma, pf);
241                    }
242                    sh.last_load_sample = Some((busy, decoded, prefilled));
243                }
244            }
245            None => sh.last_load_sample = Some((busy, decoded, prefilled)),
246        }
247        let (rate, prefill_rate) = (sh.decode_rate_ewma, sh.prefill_rate_ewma);
248
249        let queue: u64 = stats.queue_depth.iter().map(|q| q.load(std::sync::atomic::Ordering::Relaxed)).sum();
250        (
251            stats.pool_blocks_used.load(std::sync::atomic::Ordering::Relaxed),
252            stats.pool_blocks_total.load(std::sync::atomic::Ordering::Relaxed),
253            stats.lanes_active.load(std::sync::atomic::Ordering::Relaxed) as u32,
254            self.max_lanes,
255            queue as u32,
256            rate,
257            prefill_rate,
258            now,
259        )
260    }
261
262    fn capacity(&mut self) -> CapacitySummary {
263        let (kv_used, kv_total, lanes, max_lanes, queue, rate, prefill_rate, at) =
264            self.sample_load();
265        CapacitySummary {
266            gpu_memory_bytes: 0,
267            gpu_memory_free_bytes: 0,
268            host_memory_bytes: 0,
269            active_sessions: self.shared.lock().expect("agent shared").contexts.len() as u32,
270            max_sessions: u32::MAX,
271            max_context_tokens: self.daemon.max_stream_tokens(),
272            kv_blocks_used: kv_used,
273            kv_blocks_total: kv_total,
274            lanes_active: lanes,
275            max_lanes,
276            queue_depth: queue,
277            decode_tokens_per_s: rate,
278            prefill_tokens_per_s: prefill_rate,
279            sampled_at_ms: at,
280        }
281    }
282
283    fn send_status(&mut self, endpoint: &mut Endpoint) {
284        let capacity = self.capacity();
285        let msg = linkf::NodeStatusMsg {
286            mem: superfluid_proto::linkw::MemCountersMsg {
287                allocated_bytes: 0,
288                host_retained_bytes: 0,
289                pool_blocks_total: capacity.kv_blocks_total,
290                pool_blocks_used: capacity.kv_blocks_used,
291                pool_bytes_evictable: 0,
292                tentative_bytes: 0,
293            },
294            capacity,
295            loadable_models: vec![self.model_name.clone()],
296        };
297        let _ = endpoint.send(msg_type::NODE_STATUS, &msg);
298        self.last_status_ms = self.start.elapsed().as_millis() as u64;
299    }
300
301    fn ackf(&mut self) -> HelloAckF {
302        HelloAckF {
303            chosen_version: superfluid_linkf::driver::PROTO_VERSION,
304            node_identity: self.identity.clone(),
305            capacity: self.capacity(),
306            loadable_models: vec![self.model_name.clone()],
307            feature_flags: 0,
308            limits: LinkFLimits {
309                max_frame: superfluid_proto::MAX_FRAME,
310                transfer_window: 8,
311                max_transfer: 1 << 26,
312                transfer_budget: 1 << 28,
313            },
314        }
315    }
316
317    pub fn serve(&mut self, mut endpoint: Endpoint) -> Result<(), DaemonError> {
318        let hs = if self.handshake_timeout.is_zero() {
319            endpoint.answer_hello_as_agent(&self.ackf(), &self.auth)
320        } else {
321            endpoint.answer_hello_as_agent_within(&self.ackf(), self.handshake_timeout, &self.auth)
322        };
323        match hs {
324            Ok(()) => {}
325            Err(LinkFError::TimedOut) => {
326                tracing::warn!("node-agent dropped a silent connection (no Hello)");
327                return Ok(());
328            }
329            Err(LinkFError::Unauthorized) => return Err(LinkFError::Unauthorized.into()),
330            Err(e) => return Err(e.into()),
331        }
332        let heartbeat = match self.idle_timeout {
333            t if t.is_zero() => Liveness::DEFAULT.heartbeat,
334            t => Liveness::DEFAULT.heartbeat.min(t / 3),
335        };
336        endpoint.keep_alive(Liveness { heartbeat, silence: self.idle_timeout });
337        if endpoint.version() >= 3 {
338            endpoint.send(msg_type::TOKENIZER, &linkf::TokenizerMsg { digest: self.tokenizer })?;
339        }
340        // Dropped on the way out, which stops a generation still running.
341        let mut work = Work::default();
342        self.serve_link(&mut endpoint, &mut work)
343    }
344
345    fn serve_link(&mut self, endpoint: &mut Endpoint, work: &mut Work) -> Result<(), DaemonError> {
346        loop {
347            let now = self.start.elapsed();
348            self.renew(endpoint, now)?;
349            self.fence(now);
350            if now.as_millis() as u64 >= self.last_status_ms + STATUS_INTERVAL_MS {
351                self.send_status(endpoint);
352            }
353            let was_busy = work.busy();
354            if let Some(run) = work.generation.as_mut() {
355                if self.forward(endpoint, run, now)? {
356                    work.generation = None;
357                }
358            }
359            work.syncs.retain(|rx| loop {
360                match rx.try_recv() {
361                    Ok(Synced::Segment(seg)) => {
362                        let _ = endpoint.send(msg_type::STATE_SYNC_SEGMENT, &seg);
363                    }
364                    Ok(Synced::Done(resp)) => {
365                        let _ = endpoint.send(msg_type::STATE_SYNC_RESP, &resp);
366                        break false;
367                    }
368                    Err(mpsc::TryRecvError::Empty) => break true,
369                    Err(mpsc::TryRecvError::Disconnected) => break false,
370                }
371            });
372            if let Some(stage) = &work.stage {
373                match stage.try_recv() {
374                    Ok(resp) => {
375                        work.stage = None;
376                        endpoint.send(msg_type::STAGE_FORWARD_RESP, &resp)?;
377                    }
378                    Err(mpsc::TryRecvError::Empty) => {}
379                    Err(mpsc::TryRecvError::Disconnected) => work.stage = None,
380                }
381            }
382            if was_busy && !work.busy() {
383                endpoint.poll_every(POLL_IDLE)?;
384            }
385            let frames = match endpoint.pump() {
386                Ok(f) => f,
387                Err(LinkFError::Closed) => return Ok(()),
388                Err(LinkFError::TimedOut) => {
389                    tracing::warn!(
390                        quiet_ms = endpoint.quiet_for().as_millis() as u64,
391                        "node-agent dropped a connection the head went silent on"
392                    );
393                    return Ok(());
394                }
395                Err(e) => return Err(e.into()),
396            };
397            for frame in frames {
398                self.handle(endpoint, frame, work)?;
399            }
400            // A head on version 2 sends no heartbeat while it waits on a generation, so only
401            // a connection with nothing running is idle.
402            if endpoint.version() < 3
403                && !work.busy()
404                && !self.idle_timeout.is_zero()
405                && endpoint.quiet_for() >= self.idle_timeout
406            {
407                tracing::info!("node-agent dropped an idle connection");
408                return Ok(());
409            }
410        }
411    }
412
413    /// Asks the head to renew the leases of the sessions placed over this connection, which is
414    /// where it answers them; again after [`RENEW_RETRY`] until a grant comes.
415    fn renew(&mut self, endpoint: &mut Endpoint, now: Duration) -> Result<(), DaemonError> {
416        let due: Vec<_> = {
417            let sh = self.shared.lock().expect("agent shared");
418            sh.agent
419                .needs_renew(now)
420                .into_iter()
421                .filter(|r| sh.owners.get(&r.session_id) == Some(&self.conn))
422                .collect()
423        };
424        for renew in due {
425            if self.asked.get(&renew.session_id).is_some_and(|&at| now.saturating_sub(at) < RENEW_RETRY) {
426                continue;
427            }
428            endpoint.send(msg_type::LEASE_RENEW, &renew)?;
429            self.asked.insert(renew.session_id, now);
430        }
431        Ok(())
432    }
433
434    fn fence(&mut self, now: Duration) {
435        let mut sh = self.shared.lock().expect("agent shared");
436        for action in sh.agent.poll(now) {
437            match action {
438                AgentAction::SelfFence { session_id } => {
439                    tracing::warn!(session_id, "node-agent self-fenced on lease expiry");
440                    sh.contexts.remove(&session_id);
441                    sh.params.remove(&session_id);
442                    sh.owners.remove(&session_id);
443                }
444            }
445        }
446    }
447
448    fn handle(
449        &mut self,
450        endpoint: &mut Endpoint,
451        frame: superfluid_proto::Frame,
452        work: &mut Work,
453    ) -> Result<(), DaemonError> {
454        let now = self.start.elapsed();
455        match frame.msg_type {
456            msg_type::RECONCILE => {
457                let m: linkf::ReconcileMsg = postcard::from_bytes(&frame.payload)?;
458                let mut sh = self.shared.lock().expect("agent shared");
459                let (ack, retransmissions) = sh.agent.on_reconcile(&m, now);
460                for s in ack.sessions.iter().filter(|s| s.held) {
461                    sh.owners.insert(s.session_id, self.conn);
462                }
463                drop(sh);
464                endpoint.send(msg_type::RECONCILE_ACK, &ack)?;
465                for batch in retransmissions {
466                    endpoint.send(msg_type::TOKEN_EVENTS, &batch)?;
467                }
468            }
469            msg_type::SESSION_ASSIGN => {
470                let m: linkf::SessionAssignMsg = postcard::from_bytes(&frame.payload)?;
471                let mut sh = self.shared.lock().expect("agent shared");
472                if sh.agent.on_assign(&m, now) {
473                    sh.params.insert(m.session_id, (decode_params(&m.params), m.qos_class.min(crate::qos::BACKGROUND_AGENT)));
474                    sh.owners.insert(m.session_id, self.conn);
475                    self.asked.remove(&m.session_id);
476                }
477            }
478            msg_type::SESSION_REVOKE => {
479                let m: linkf::SessionRevokeMsg = postcard::from_bytes(&frame.payload)?;
480                let mut sh = self.shared.lock().expect("agent shared");
481                if sh.agent.on_revoke(&m) {
482                    sh.contexts.remove(&m.session_id);
483                    sh.params.remove(&m.session_id);
484                    sh.owners.remove(&m.session_id);
485                    self.asked.remove(&m.session_id);
486                }
487            }
488            msg_type::APPEND_CONTENT => {
489                let m: linkf::AppendContentMsg = postcard::from_bytes(&frame.payload)?;
490                let mut sh = self.shared.lock().expect("agent shared");
491                if sh.agent.on_append(m.session_id, m.epoch, m.command_id) {
492                    let tokens = decode_tokens(&m.blocks)?;
493                    sh.contexts.insert(m.session_id, tokens);
494                }
495            }
496            msg_type::GENERATE_REQ => {
497                let m: linkf::GenerateReqMsg = postcard::from_bytes(&frame.payload)?;
498                let declined = work.generation.is_some() || !self.shared.lock().expect("agent shared").agent.on_generate(&m);
499                if declined {
500                    tracing::warn!(
501                        session = m.session_id,
502                        generation = m.generation_id,
503                        epoch = m.epoch,
504                        "declining a generate (unknown session, superseded epoch, replayed \
505                         command, or one already running on this connection); reporting it \
506                         rather than going silent"
507                    );
508                    let msg = linkf::TokenEventsMsg {
509                        events: vec![linkf::TokenEvent {
510                            session_id: m.session_id,
511                            epoch: m.epoch,
512                            generation_id: m.generation_id,
513                            event_seq: 0,
514                            payload: TokenEventPayload::Finish {
515                                reason: FINISH_DECLINED,
516                            },
517                        }],
518                    };
519                    endpoint.send(msg_type::TOKEN_EVENTS, &msg)?;
520                } else {
521                    work.generation = Some(self.start_generation(&m));
522                    endpoint.poll_every(POLL_RUNNING)?;
523                }
524            }
525            msg_type::CONTROL_ASK => {
526                if let Some(dial) = &self.on_control_ask {
527                    dial();
528                }
529            }
530            msg_type::TOKENIZER_EXPORT_REQ => {
531                let m: linkf::TokenizerExportReqMsg = postcard::from_bytes(&frame.payload)?;
532                endpoint.send(msg_type::TOKENIZER_EXPORT, &self.export(m.request_id))?;
533            }
534            msg_type::TOKENIZE_REQ => {
535                let m: linkf::TokenizeReqMsg = postcard::from_bytes(&frame.payload)?;
536                let tokens = self.tokenizer_impl.as_ref().and_then(|t| match &m.text {
537                    linkf::TokenizeMsg::Encode(text) => Some(t.encode(text)),
538                    linkf::TokenizeMsg::Plain(text) => t.encode_plain(text),
539                    linkf::TokenizeMsg::Pieces(pieces) => {
540                        let pieces: Vec<(&str, bool)> = pieces.iter().map(|(p, s)| (p.as_str(), *s)).collect();
541                        t.encode_pieces(&pieces)
542                    }
543                });
544                endpoint.send(msg_type::TOKENIZE_RESP, &linkf::TokenizeRespMsg { request_id: m.request_id, tokens })?;
545            }
546            msg_type::GENERATE_OPTS => {
547                let m: linkf::GenerateOptsMsg = postcard::from_bytes(&frame.payload)?;
548                self.opts.insert((m.session_id, m.generation_id), m);
549            }
550            msg_type::XFER_BEGIN => {
551                let m: linkf::XferBeginMsg = postcard::from_bytes(&frame.payload)?;
552                if m.total_len > MAX_BLOB {
553                    return Err(DaemonError::Protocol("the head sent a blob larger than a node takes"));
554                }
555                self.incoming.insert(m.transfer_id, (m.content_hash, Vec::with_capacity(m.total_len as usize)));
556            }
557            msg_type::XFER_CHUNK => {
558                let m: linkf::XferChunkMsg = postcard::from_bytes(&frame.payload)?;
559                if let Some((_, bytes)) = self.incoming.get_mut(&m.transfer_id) {
560                    if m.offset != bytes.len() as u64 || bytes.len() as u64 + m.data.len() as u64 > MAX_BLOB {
561                        return Err(DaemonError::Protocol("a blob's pieces came out of order"));
562                    }
563                    bytes.extend_from_slice(&m.data);
564                }
565            }
566            msg_type::XFER_END => {
567                let m: linkf::XferEndMsg = postcard::from_bytes(&frame.payload)?;
568                if let Some((want, bytes)) = self.incoming.remove(&m.transfer_id) {
569                    let want: String = want.iter().map(|b| format!("{b:02x}")).collect();
570                    if crate::media::content_hash(&bytes) != want {
571                        return Err(DaemonError::Protocol("a blob from the head is not what its hash says"));
572                    }
573                    // A blob this node cannot keep fails the request that brings it, not the link.
574                    if let Err(e) = self.daemon.media_put(&bytes) {
575                        tracing::warn!("node-agent cannot keep a blob from the head: {e}");
576                    }
577                }
578            }
579            msg_type::STATE_SYNC_REQ => {
580                let mut m: linkf::StateSyncReqF = postcard::from_bytes(&frame.payload)?;
581                self.localize(&mut m.req);
582                let (tx, rx) = mpsc::channel();
583                let daemon = Arc::clone(&self.daemon);
584                std::thread::Builder::new()
585                    .name("node-state-sync".into())
586                    .spawn(move || {
587                        let request_id = m.request_id;
588                        let streamed = matches!(m.req, superfluid_proto::linkw::StateSyncReqMsg::Transcribe { stream: true, .. });
589                        let result = if streamed {
590                            let (seg_tx, seg_rx) = mpsc::channel();
591                            let told = tx.clone();
592                            let tell = std::thread::spawn(move || {
593                                for segment in seg_rx {
594                                    let _ = told.send(Synced::Segment(linkf::StateSyncSegmentF { request_id, segment }));
595                                }
596                            });
597                            let result = daemon.state_sync_stream(m.req, seg_tx);
598                            let _ = tell.join();
599                            result
600                        } else {
601                            daemon.state_sync(m.req)
602                        };
603                        let result = result.map_err(crate::fleet_daemon::message);
604                        let _ = tx.send(Synced::Done(linkf::StateSyncRespF { request_id, result }));
605                    })
606                    .expect("spawn a node state operation");
607                work.syncs.push(rx);
608                endpoint.poll_every(POLL_RUNNING)?;
609            }
610            msg_type::GENERATE_CANCEL => {
611                let m: linkf::GenerateCancelMsg = postcard::from_bytes(&frame.payload)?;
612                if let Some(run) = work
613                    .generation
614                    .as_mut()
615                    .filter(|r| r.session_id == m.session_id && r.generation_id == m.generation_id)
616                {
617                    run.cancelled = true;
618                    run.cancel.store(true, Ordering::Relaxed);
619                }
620            }
621            msg_type::STAGE_FORWARD_REQ => {
622                let m: linkf::StageForwardReqMsg = postcard::from_bytes(&frame.payload)?;
623                if work.stage.is_some() {
624                    // The head waits on one stage per connection; another is answered as one
625                    // the node could not run.
626                    let resp = linkf::StageForwardRespMsg { request_id: m.request_id, hidden: None, token: None };
627                    endpoint.send(msg_type::STAGE_FORWARD_RESP, &resp)?;
628                } else {
629                    work.stage = Some(self.start_stage(m));
630                    endpoint.poll_every(POLL_RUNNING)?;
631                }
632            }
633            msg_type::WATERMARK_ACK => {
634                let m: linkf::WatermarkAckMsg = postcard::from_bytes(&frame.payload)?;
635                self.shared.lock().expect("agent shared").agent.on_watermark_ack(&m);
636            }
637            msg_type::LEASE_GRANT => {
638                let m: linkf::LeaseGrantMsg = postcard::from_bytes(&frame.payload)?;
639                self.shared.lock().expect("agent shared").agent.on_lease_grant(&m, now);
640                self.asked.remove(&m.session_id);
641            }
642            _ => {  }
643        }
644        Ok(())
645    }
646
647    /// The tokenizer as a head without the model takes it; no vocabulary when this node has none.
648    fn export(&self, request_id: u64) -> linkf::TokenizerExportMsg {
649        let Some(t) = &self.tokenizer_impl else {
650            return linkf::TokenizerExportMsg {
651                request_id,
652                digest: 0,
653                vocab: Vec::new(),
654                specials: Vec::new(),
655                bos: None,
656                eos: 0,
657                chat_template: String::new(),
658                named_templates: Vec::new(),
659                plain: false,
660                pieces: false,
661            };
662        };
663        linkf::TokenizerExportMsg {
664            request_id,
665            digest: self.tokenizer,
666            vocab: (0..t.vocab_size()).map(|i| t.token_bytes(i)).collect(),
667            specials: t.special_tokens(),
668            bos: t.bos_token(),
669            eos: t.eos_token(),
670            chat_template: t.chat_template_jinja(),
671            named_templates: ["tool_use"].iter().filter_map(|n| Some((n.to_string(), t.chat_template_named(n)?))).collect(),
672            plain: t.encode_plain("a").is_some(),
673            pieces: t.encode_pieces(&[("a", false)]).is_some(),
674        }
675    }
676
677    /// A path the head sent as a blob, where the blob sits in this node's media pool.
678    fn local(&self, path: &str) -> String {
679        match path.strip_prefix(linkf::BLOB_PREFIX) {
680            Some(hash) if crate::media::MediaPool::is_valid_id(hash) => self.daemon.media_path(hash).to_string_lossy().into_owned(),
681            _ => path.to_string(),
682        }
683    }
684
685    fn localize(&self, req: &mut superfluid_proto::linkw::StateSyncReqMsg) {
686        use superfluid_proto::linkw::StateSyncReqMsg as R;
687        match req {
688            R::MediaProbe { image_path } | R::MediaEncode { image_path } => *image_path = self.local(image_path),
689            R::Transcribe { audio_path, .. } => *audio_path = self.local(audio_path),
690            _ => {}
691        }
692    }
693
694    /// Runs a stage forward on a thread of its own, as a generation runs, so a long one does not
695    /// silence the connection.
696    fn start_stage(&self, m: linkf::StageForwardReqMsg) -> mpsc::Receiver<linkf::StageForwardRespMsg> {
697        let (tx, rx) = mpsc::channel();
698        let daemon = Arc::clone(&self.daemon);
699        std::thread::Builder::new()
700            .name("node-stage".into())
701            .spawn(move || {
702                let (hidden, token) = match daemon.forward_stage(m.tokens, m.start_layer, m.end_layer, m.hidden_in) {
703                    Ok(crate::StageOutput::Hidden(h)) => (Some(h), None),
704                    Ok(crate::StageOutput::Token(t)) => (None, Some(t)),
705                    Err(_) => (None, None),
706                };
707                let _ = tx.send(linkf::StageForwardRespMsg { request_id: m.request_id, hidden, token });
708            })
709            .expect("spawn a node stage");
710        rx
711    }
712
713    /// Starts `req` on a thread of its own, so this connection goes on reading the head
714    /// (cancels, lease grants, heartbeats) and sending status while it runs.
715    fn start_generation(&mut self, req: &linkf::GenerateReqMsg) -> Running {
716        let (context, (params, class)) = {
717            let sh = self.shared.lock().expect("agent shared");
718            (
719                sh.contexts.get(&req.session_id).cloned().unwrap_or_default(),
720                sh.params.get(&req.session_id).copied().unwrap_or((GenParams::default(), crate::qos::FOREGROUND_AGENT)),
721            )
722        };
723        let max_new = req.budgets.max_new_tokens.min(u32::MAX as u64) as u32;
724        let max_wall_ms = req.budgets.max_wall_ms;
725        let opts = self.opts.remove(&(req.session_id, req.generation_id));
726        self.opts.clear();
727        let media: Vec<(u64, std::path::PathBuf, u32)> = opts
728            .iter()
729            .flat_map(|o| &o.media)
730            .map(|(offset, hash, n)| (*offset, std::path::PathBuf::from(self.local(&format!("{}{hash}", linkf::BLOB_PREFIX))), *n))
731            .collect();
732        let cancel = Arc::new(AtomicBool::new(false));
733        let (tx, out) = mpsc::channel();
734        let (daemon, shared, stop) = (Arc::clone(&self.daemon), Arc::clone(&self.shared), Arc::clone(&cancel));
735        shared.lock().expect("agent shared").busy.start();
736        std::thread::Builder::new()
737            .name("node-generation".into())
738            .spawn(move || {
739                let generated = match Extras::of(&daemon, opts.as_ref()) {
740                    Ok(extras) => {
741                        let lp = tx.clone();
742                        let generated = daemon.generate_tokens_streaming_lp(
743                            context,
744                            params,
745                            max_new,
746                            max_wall_ms,
747                            class,
748                            extras.extras.clone(),
749                            media,
750                            |batch| {
751                                if stop.load(Ordering::Relaxed) || tx.send(Out::Tokens(batch.to_vec())).is_err() {
752                                    return std::ops::ControlFlow::Break(());
753                                }
754                                std::ops::ControlFlow::Continue(())
755                            },
756                            |items| {
757                                let _ = lp.send(Out::Logprobs(items.to_vec()));
758                            },
759                        );
760                        extras.free(&daemon);
761                        generated
762                    }
763                    Err(e) => Err(e),
764                };
765                shared.lock().expect("agent shared").busy.stop();
766                let _ = tx.send(Out::Done(generated));
767            })
768            .expect("spawn a node generation");
769        Running {
770            session_id: req.session_id,
771            generation_id: req.generation_id,
772            max_new,
773            cancel,
774            cancelled: false,
775            out,
776            pending: Vec::with_capacity(EMIT_COALESCE_TOKENS),
777            last_flush: self.start.elapsed(),
778            sent: false,
779        }
780    }
781
782    /// Sends what `run` produced since the last pass, coalesced, and its finish once it ends;
783    /// `true` once it has ended, or was fenced and goes silent.
784    fn forward(&mut self, endpoint: &mut Endpoint, run: &mut Running, now: Duration) -> Result<bool, DaemonError> {
785        let mut done = None;
786        loop {
787            match run.out.try_recv() {
788                Ok(Out::Tokens(t)) => {
789                    run.pending.extend_from_slice(&t);
790                    if run.pending.len() >= EMIT_COALESCE_TOKENS && !self.flush(endpoint, run, now)? {
791                        return Ok(true);
792                    }
793                }
794                Ok(Out::Logprobs(items)) => {
795                    let msgs = items
796                        .into_iter()
797                        .map(|lp| linkf::TokenLogprobMsg {
798                            token: lp.token,
799                            logprob_bits: lp.logprob.to_bits(),
800                            top: lp.top.into_iter().map(|(t, p)| (t, p.to_bits())).collect(),
801                        })
802                        .collect();
803                    let emitted = self.shared.lock().expect("agent shared").agent.emit(
804                        run.session_id,
805                        run.generation_id,
806                        TokenEventPayload::Logprobs(msgs),
807                    );
808                    match emitted {
809                        Some(batch) => endpoint.send(msg_type::TOKEN_EVENTS, &batch)?,
810                        None => return Ok(true),
811                    }
812                }
813                Ok(Out::Done(r)) => {
814                    done = Some(r);
815                    break;
816                }
817                Err(mpsc::TryRecvError::Empty) => break,
818                Err(mpsc::TryRecvError::Disconnected) => {
819                    done = Some(Err(DaemonError::Generation("the generation ended without a result".into())));
820                    break;
821                }
822            }
823        }
824        let due = done.is_some() || !run.sent || now.saturating_sub(run.last_flush).as_millis() as u64 >= EMIT_COALESCE_MS;
825        if due && !run.pending.is_empty() && !self.flush(endpoint, run, now)? {
826            return Ok(true);
827        }
828        let mut sh = self.shared.lock().expect("agent shared");
829        if sh.agent.is_fenced(run.session_id) != Some(false) {
830            return Ok(true);
831        }
832        let Some(generated) = done else { return Ok(false) };
833        let reason = match generated {
834            Ok(_) if run.cancelled => finish::CANCELLED,
835            Ok((_, _, true)) => FINISH_EXPIRED,
836            Ok((tokens, _, _)) if tokens.len() as u32 >= run.max_new => finish::LENGTH,
837            Ok(_) => finish::EOS,
838            Err(e) if is_client_refusal(&e) => {
839                tracing::warn!(
840                    session = run.session_id,
841                    generation = run.generation_id,
842                    error = %e,
843                    "refusing a generation the node cannot stage; \
844                     the head should have caught this before dispatch"
845                );
846                FINISH_REFUSED
847            }
848            Err(e) => return Err(e),
849        };
850        if let Some(batch) = sh.agent.emit(run.session_id, run.generation_id, TokenEventPayload::Finish { reason }) {
851            endpoint.send(msg_type::TOKEN_EVENTS, &batch)?;
852        }
853        Ok(true)
854    }
855
856    /// Sends `run`'s pending tokens; `false` when its session is fenced, which sends nothing.
857    fn flush(&mut self, endpoint: &mut Endpoint, run: &mut Running, now: Duration) -> Result<bool, DaemonError> {
858        run.last_flush = now;
859        run.sent = true;
860        let tokens = std::mem::take(&mut run.pending);
861        let batch = self.shared.lock().expect("agent shared").agent.emit(
862            run.session_id,
863            run.generation_id,
864            TokenEventPayload::Tokens(tokens),
865        );
866        match batch {
867            Some(batch) => {
868                endpoint.send(msg_type::TOKEN_EVENTS, &batch)?;
869                Ok(true)
870            }
871            None => Ok(false),
872        }
873    }
874}
875
876/// What a connection has running off its own thread.
877#[derive(Default)]
878struct Work {
879    generation: Option<Running>,
880    /// A stage forward's answer, once it has one.
881    stage: Option<mpsc::Receiver<linkf::StageForwardRespMsg>>,
882    /// State operations' answers, once each has one.
883    syncs: Vec<mpsc::Receiver<Synced>>,
884}
885
886/// What a state operation has to tell the head: a streamed transcription's segments, then its
887/// answer.
888enum Synced {
889    Segment(linkf::StateSyncSegmentF),
890    Done(linkf::StateSyncRespF),
891}
892
893impl Work {
894    fn busy(&self) -> bool {
895        self.generation.is_some() || self.stage.is_some() || !self.syncs.is_empty()
896    }
897}
898
899/// A generation running on a connection; dropping it stops the generation at its next step.
900struct Running {
901    session_id: u64,
902    generation_id: u64,
903    max_new: u32,
904    cancel: Arc<AtomicBool>,
905    /// The head asked for the stop.
906    cancelled: bool,
907    out: mpsc::Receiver<Out>,
908    pending: Vec<u32>,
909    last_flush: Duration,
910    /// Tokens have gone to the head.
911    sent: bool,
912}
913
914impl Drop for Running {
915    fn drop(&mut self) {
916        self.cancel.store(true, Ordering::Relaxed);
917    }
918}
919
920enum Out {
921    Tokens(Vec<u32>),
922    Logprobs(Vec<crate::scheduler::TokenLogprob>),
923    Done(Result<(Vec<u32>, u32, bool), DaemonError>),
924}
925
926/// A generation's extras as its options ask, with the grammar and logit bias compiled on this
927/// node's engine and freed once it ends.
928struct Extras {
929    extras: crate::scheduler::GenExtras,
930}
931
932impl Extras {
933    fn of(daemon: &Daemon, opts: Option<&linkf::GenerateOptsMsg>) -> Result<Extras, DaemonError> {
934        // Each prompt stays in the cache for the conversation's next turn, and with --park on
935        // disk, so a restarted node starts it warm too.
936        let mut extras = crate::scheduler::GenExtras { keep_prefix: true, ..Default::default() };
937        let Some(o) = opts else { return Ok(Extras { extras }) };
938        extras.presence_penalty = f32::from_bits(o.presence_penalty_bits);
939        extras.frequency_penalty = f32::from_bits(o.frequency_penalty_bits);
940        extras.repeat_penalty = f32::from_bits(o.repeat_penalty_bits);
941        extras.ignore_eos = o.ignore_eos;
942        if let Some(top) = o.top_logprobs {
943            extras.want_logprobs = true;
944            extras.top_logprobs = top;
945        }
946        extras.grammar_handle = match &o.grammar {
947            Some(linkf::GrammarSpecMsg::JsonSchema(schema)) => daemon.grammar_create(schema)?,
948            Some(linkf::GrammarSpecMsg::Structural(tag)) => daemon.grammar_create_structural(tag)?,
949            None => 0,
950        };
951        if let Some((tokens, values)) = &o.logit_bias {
952            let values: Vec<f32> = values.iter().map(|b| f32::from_bits(*b)).collect();
953            match daemon.logit_bias_create(tokens, &values) {
954                Ok(h) => extras.logit_bias_handle = h,
955                Err(e) => {
956                    if extras.grammar_handle != 0 {
957                        daemon.grammar_free(extras.grammar_handle);
958                    }
959                    return Err(e);
960                }
961            }
962        }
963        Ok(Extras { extras })
964    }
965
966    fn free(&self, daemon: &Daemon) {
967        if self.extras.grammar_handle != 0 {
968            daemon.grammar_free(self.extras.grammar_handle);
969        }
970        if self.extras.logit_bias_handle != 0 {
971            daemon.logit_bias_free(self.extras.logit_bias_handle);
972        }
973    }
974}
975
976fn is_client_refusal(e: &DaemonError) -> bool {
977    matches!(
978        e,
979        DaemonError::StreamTooLong { .. } | DaemonError::Constraint(_)
980    )
981}