1use 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
25pub const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
28
29struct AgentShared {
30 agent: AgentSession,
31 owners: HashMap<u64, u64>,
33 contexts: HashMap<u64, Vec<u32>>,
34 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#[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 asked: HashMap<u64, Duration>,
87 opts: HashMap<(u64, u64), linkf::GenerateOptsMsg>,
89 incoming: HashMap<u64, ([u8; 32], Vec<u8>)>,
91 tokenizer: u64,
93 tokenizer_impl: Option<Arc<dyn superfluid_engine::Tokenizer>>,
95 on_control_ask: Option<Arc<dyn Fn() + Send + Sync>>,
97}
98
99const MAX_BLOB: u64 = 1 << 30;
101
102static NEXT_CONN: AtomicU64 = AtomicU64::new(1);
103
104const STATUS_INTERVAL_MS: u64 = 2_000;
105
106const RENEW_RETRY: Duration = Duration::from_secs(1);
108
109const POLL_RUNNING: Duration = Duration::from_millis(2);
112
113const POLL_IDLE: Duration = Duration::from_millis(20);
114
115const 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 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 pub fn with_tokenizer(mut self, digest: u64) -> NodeAgent {
191 self.tokenizer = digest;
192 self
193 }
194
195 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 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 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 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 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 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 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 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 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 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 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 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 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#[derive(Default)]
878struct Work {
879 generation: Option<Running>,
880 stage: Option<mpsc::Receiver<linkf::StageForwardRespMsg>>,
882 syncs: Vec<mpsc::Receiver<Synced>>,
884}
885
886enum 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
899struct Running {
901 session_id: u64,
902 generation_id: u64,
903 max_new: u32,
904 cancel: Arc<AtomicBool>,
905 cancelled: bool,
907 out: mpsc::Receiver<Out>,
908 pending: Vec<u32>,
909 last_flush: Duration,
910 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
926struct Extras {
929 extras: crate::scheduler::GenExtras,
930}
931
932impl Extras {
933 fn of(daemon: &Daemon, opts: Option<&linkf::GenerateOptsMsg>) -> Result<Extras, DaemonError> {
934 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}