1use std::os::unix::net::UnixStream;
2use std::path::PathBuf;
3use std::thread::JoinHandle;
4
5use superfluid_agent::WorkerClient;
6use superfluid_engine::Engine;
7use superfluid_proto::linkw;
8use superfluid_worker::{WorkerConfig, WorkerServer};
9
10use crate::DaemonError;
11
12#[derive(Debug, Clone)]
13pub struct WorkerSpec {
14 pub bin: PathBuf,
15 pub args: Vec<String>,
16 pub stderr: Option<std::sync::Arc<std::os::fd::OwnedFd>>,
17}
18
19pub use superfluid_worker::ring_specs_for;
20
21enum Backend {
22 Thread(Option<JoinHandle<()>>),
23 Process {
24 child: std::process::Child,
25 spec: WorkerSpec,
26 },
27}
28
29pub struct EngineHost {
30 client: Option<WorkerClient>,
31 backend: Backend,
32 generation: u64,
33 caps: SharedCapabilities,
34 record_change: Option<String>,
35 yield_slot: YieldSlot,
36}
37
38pub type YieldSlot = std::sync::Arc<std::sync::RwLock<Option<superfluid_shm::YieldSignal>>>;
41
42fn yield_slot_of(client: &WorkerClient) -> YieldSlot {
43 std::sync::Arc::new(std::sync::RwLock::new(client.yield_signal()))
44}
45
46pub fn set_latency(slot: &YieldSlot, on: bool) {
48 if let Some(s) = slot.read().expect("yield slot").as_ref() {
49 s.set_latency(on);
50 }
51}
52
53pub fn raise_yield(slot: &YieldSlot) {
55 if let Some(s) = slot.read().expect("yield slot").as_ref() {
56 s.raise();
57 }
58}
59
60pub type SharedCapabilities = std::sync::Arc<std::sync::RwLock<crate::capabilities::Capabilities>>;
61
62fn caps_of(client: &WorkerClient) -> SharedCapabilities {
63 std::sync::Arc::new(std::sync::RwLock::new(crate::capabilities::Capabilities::parse(&client.hello.capabilities)))
64}
65
66impl EngineHost {
67 pub fn spawn<E, F>(make_engine: F) -> Result<EngineHost, DaemonError>
68 where
69 E: Engine + 'static,
70 F: FnOnce() -> (E, Option<Vec<linkw::RingSpec>>) + Send + 'static,
71 {
72 let (frames_a, frames_w) = UnixStream::pair()?;
73 let (fds_a, fds_w) = UnixStream::pair()?;
74 let handle = std::thread::Builder::new()
75 .name("engine-worker".into())
76 .spawn(move || {
77 let (engine, ring_specs) = make_engine();
78 let mut server = WorkerServer::new(
79 engine,
80 WorkerConfig {
81 engine_bundle_hash: [0; 32],
82 },
83 frames_w,
84 fds_w,
85 );
86 if let Some(specs) = ring_specs {
87 server = server.with_ring_specs(specs);
88 }
89 if let Err(e) = server.serve() {
90 tracing::error!("engine-worker exited with error: {e}");
91 }
92 })?;
93 let client = WorkerClient::connect(frames_a, fds_a)?;
94 Ok(EngineHost {
95 caps: caps_of(&client),
96 yield_slot: yield_slot_of(&client),
97 client: Some(client),
98 backend: Backend::Thread(Some(handle)),
99 generation: 0,
100 record_change: None,
101 })
102 }
103
104 pub fn try_spawn<E, F>(make_engine: F) -> Result<EngineHost, DaemonError>
105 where
106 E: Engine + 'static,
107 F: FnOnce() -> Result<(E, Option<Vec<linkw::RingSpec>>), String> + Send + 'static,
108 {
109 EngineHost::try_spawn_loaded(move || make_engine().map(|(engine, rings)| superfluid_adapter_kit::Loaded::engine(engine, rings)))
110 }
111
112 pub fn try_spawn_loaded<F>(open: F) -> Result<EngineHost, DaemonError>
113 where
114 F: FnOnce() -> Result<superfluid_adapter_kit::Loaded, String> + Send + 'static,
115 {
116 let (frames_a, frames_w) = UnixStream::pair()?;
117 let (fds_a, fds_w) = UnixStream::pair()?;
118 let (tx, rx) = std::sync::mpsc::channel::<Result<(), String>>();
119 let handle = std::thread::Builder::new()
120 .name("engine-worker".into())
121 .spawn(move || {
122 let loaded = match open() {
123 Ok(v) => {
124 let _ = tx.send(Ok(()));
125 v
126 }
127 Err(e) => {
128 let _ = tx.send(Err(e));
129 return;
130 }
131 };
132 if let Err(e) = loaded.serve(frames_w, fds_w) {
133 tracing::error!("engine-worker exited with error: {e}");
134 }
135 })?;
136 match rx.recv() {
137 Ok(Ok(())) => {}
138 Ok(Err(msg)) => {
139 let _ = handle.join();
140 return Err(DaemonError::EngineLoad(msg));
141 }
142 Err(_) => {
143 let _ = handle.join();
144 return Err(DaemonError::EngineLoad(
145 "engine worker exited before reporting a load result".into(),
146 ));
147 }
148 }
149 let client = WorkerClient::connect(frames_a, fds_a)?;
150 Ok(EngineHost {
151 caps: caps_of(&client),
152 yield_slot: yield_slot_of(&client),
153 client: Some(client),
154 backend: Backend::Thread(Some(handle)),
155 generation: 0,
156 record_change: None,
157 })
158 }
159
160 pub fn spawn_process(spec: WorkerSpec) -> Result<EngineHost, DaemonError> {
161 let (child, client) = launch(&spec)?;
162 Ok(EngineHost {
163 caps: caps_of(&client),
164 yield_slot: yield_slot_of(&client),
165 client: Some(client),
166 backend: Backend::Process { child, spec },
167 generation: 0,
168 record_change: None,
169 })
170 }
171
172 pub fn yield_slot(&self) -> YieldSlot {
173 std::sync::Arc::clone(&self.yield_slot)
174 }
175
176 pub fn clear_yield(&self) {
178 if let Some(s) = self.yield_slot.read().expect("yield slot").as_ref() {
179 s.clear();
180 }
181 }
182
183 pub fn is_process(&self) -> bool {
184 matches!(self.backend, Backend::Process { .. })
185 }
186
187 pub fn worker_pid(&self) -> Option<u32> {
188 match &self.backend {
189 Backend::Process { child, .. } => Some(child.id()),
190 Backend::Thread(_) => None,
191 }
192 }
193
194 pub fn respawn(&mut self) -> Result<(), DaemonError> {
195 let Backend::Process { child, spec } = &mut self.backend else {
196 return Err(DaemonError::Protocol(
197 "in-process worker shares the daemon's fate; nothing to respawn",
198 ));
199 };
200 drop(self.client.take());
201 let _ = child.kill();
202 let _ = child.wait();
203 let (new_child, client) = launch(spec)?;
204 *child = new_child;
205 let fresh = crate::capabilities::Capabilities::parse(&client.hello.capabilities);
206 let mut held = self.caps.write().expect("capability record");
207 let changed = held.changes(&fresh);
208 if !changed.is_empty() {
209 tracing::warn!(changed = %changed.join(", "), "the respawned worker describes a different runtime or model");
210 self.record_change = Some(format!("the respawned worker's capability record differs ({})", changed.join(", ")));
211 }
212 *held = fresh;
213 drop(held);
214 *self.yield_slot.write().expect("yield slot") = client.yield_signal();
215 self.client = Some(client);
216 self.generation += 1;
217 Ok(())
218 }
219
220 pub fn take_record_change(&mut self) -> Option<String> {
221 self.record_change.take()
222 }
223
224 pub fn max_seqs(&self) -> Option<u64> {
225 self.caps.read().expect("capability record").limit("max_seqs")
226 }
227
228 pub fn exact_seeds_only(&self) -> bool {
229 matches!(self.caps.read().expect("capability record").get("state", "rollback"), crate::capabilities::Cap::No(_))
230 }
231
232 pub fn needs_prefix_checkpoints(&self) -> bool {
235 matches!(self.caps.read().expect("capability record").get("state", "shared_prefix_seed"), crate::capabilities::Cap::No(_))
236 }
237
238 pub fn park_refused(&self) -> Option<String> {
239 match self.caps.read().expect("capability record").get("serving", "park_lossless") {
240 crate::capabilities::Cap::No(why) => Some(why.to_string()),
241 _ => None,
242 }
243 }
244
245 pub fn round_granular(&self) -> bool {
246 matches!(self.caps.read().expect("capability record").get("serving", "round_granular"), crate::capabilities::Cap::Yes)
247 }
248
249 pub fn lossy_park_allowed(&self) -> bool {
250 !matches!(self.caps.read().expect("capability record").get("serving", "park_lossy"), crate::capabilities::Cap::No(_))
251 }
252
253 pub fn capabilities(&self) -> SharedCapabilities {
254 std::sync::Arc::clone(&self.caps)
255 }
256
257 pub fn generation(&self) -> u64 {
258 self.generation
259 }
260
261 pub fn is_alive(&self) -> bool {
262 self.client.is_some()
263 }
264
265 pub fn client(&mut self) -> &mut WorkerClient {
266 self.client
267 .as_mut()
268 .expect("caller must gate on is_alive() after a failed respawn")
269 }
270
271 fn client_ref(&self) -> Option<&WorkerClient> {
272 self.client.as_ref()
273 }
274
275 pub fn kv_bits(&self) -> u32 {
276 self.client_ref().map(|c| c.hello.kv_bits).unwrap_or(0)
277 }
278
279 pub fn page_size(&self) -> Option<u64> {
280 self.client_ref()?
281 .hello
282 .state_space_descriptors
283 .iter()
284 .find(|d| d.page_size_tokens > 0)
285 .map(|d| d.page_size_tokens as u64)
286 }
287
288 pub fn has_unservable_space(&self) -> bool {
289 self.client_ref()
290 .map(|c| {
291 c.hello
292 .state_space_descriptors
293 .iter()
294 .any(|d| d.flags & superfluid_abi::space_flag::OPS_UNAVAILABLE != 0)
295 })
296 .unwrap_or(false)
297 }
298
299 pub fn has_recurrent_space(&self) -> bool {
300 self.client_ref()
301 .map(|c| {
302 c.hello
303 .state_space_descriptors
304 .iter()
305 .any(|d| d.kind == superfluid_abi::space_kind::RECURRENT_BLOB)
306 })
307 .unwrap_or(false)
308 }
309
310 pub fn cache_serves_all_spaces(&self) -> bool {
311 self.client_ref()
312 .map(|c| {
313 c.hello
314 .state_space_descriptors
315 .iter()
316 .all(|d| d.flags & superfluid_abi::space_flag::NO_PREFIX_CACHE == 0)
317 })
318 .unwrap_or(false)
319 }
320
321 pub fn parkable_spaces(&self) -> Vec<(u32, u32, u32, u32)> {
322 let mut v: Vec<(u32, u32, u32, u32)> = self
323 .client_ref()
324 .map(|c| {
325 c.hello
326 .state_space_descriptors
327 .iter()
328 .filter(|d| {
329 d.flags & superfluid_abi::space_flag::OPS_UNAVAILABLE == 0
330 && d.kind != superfluid_abi::space_kind::ENCODER_CACHE
331 })
332 .map(|d| {
333 (
334 d.space_id,
335 d.kind,
336 d.snapshot_cadence,
337 d.snapshot_interval_tokens,
338 )
339 })
340 .collect()
341 })
342 .unwrap_or_default();
343 v.sort_by_key(|&(id, kind, _, _)| (kind != superfluid_abi::space_kind::PAGED_TOKEN_KV, id));
344 v
345 }
346
347 pub fn park_boundary(&self, len: u64) -> u64 {
348 let mut b = len;
349 for &(_, kind, cadence, interval) in &self.parkable_spaces() {
350 if kind == superfluid_abi::space_kind::PAGED_TOKEN_KV {
351 continue;
352 }
353 if cadence == 2 && interval > 0 {
354 b = b.min(len / interval as u64 * interval as u64);
355 }
356 }
357 b
358 }
359
360 pub fn multi_space(&self) -> bool {
361 self.parkable_spaces()
362 .iter()
363 .any(|&(_, kind, _, _)| kind != superfluid_abi::space_kind::PAGED_TOKEN_KV)
364 }
365
366 pub fn kv_block_bytes(&self) -> Option<u64> {
367 self.client_ref()?
368 .hello
369 .state_space_descriptors
370 .iter()
371 .find(|d| d.page_size_tokens > 0)
372 .map(|d| d.bytes_per_token * d.page_size_tokens as u64)
373 }
374
375 pub fn paged_spaces(&self) -> Vec<(u32, u64, u64)> {
376 let Some(c) = self.client_ref() else {
377 return Vec::new();
378 };
379 c.hello
380 .state_space_descriptors
381 .iter()
382 .filter(|d| d.page_size_tokens > 0)
383 .filter(|d| d.kind != superfluid_abi::space_kind::ENCODER_CACHE)
384 .map(|d| {
385 let page = d.page_size_tokens as u64;
386 (d.space_id, page, d.bytes_per_token * page)
387 })
388 .collect()
389 }
390
391 pub fn blob_spaces(&self) -> Vec<(u32, u64)> {
392 let Some(c) = self.client_ref() else {
393 return Vec::new();
394 };
395 c.hello
396 .state_space_descriptors
397 .iter()
398 .filter(|d| d.page_size_tokens == 0 && d.blob_bytes > 0)
399 .filter(|d| d.kind != superfluid_abi::space_kind::ENCODER_CACHE)
400 .map(|d| (d.space_id, d.blob_bytes))
401 .collect()
402 }
403
404 pub fn space_shape(&self, space_id: u32) -> Option<(u32, u32)> {
405 self.client_ref()?
406 .hello
407 .state_space_descriptors
408 .iter()
409 .find(|d| d.space_id == space_id)
410 .map(|d| (d.kind, d.page_size_tokens))
411 }
412
413 pub fn max_stream_tokens(&self) -> u64 {
414 stream_ceiling(self.staged_tokens(), &self.caps.read().expect("capability record"))
415 }
416
417 pub fn staged_tokens(&self) -> u64 {
418 self.client_ref()
419 .map(|c| {
420 c.hello
421 .limits
422 .ring_specs
423 .iter()
424 .find(|s| s.ring_id == superfluid_shm::TOKEN_RING_IN)
425 .map(|s| ((s.slot_bytes.saturating_sub(16)) / 4) as u64)
426 .unwrap_or(0)
427 })
428 .unwrap_or(0)
429 }
430}
431
432pub(crate) fn stream_ceiling(staged: u64, caps: &crate::capabilities::Capabilities) -> u64 {
433 match caps.limit("max_seq_len") {
434 Some(held) if staged > 0 => staged.min(held),
435 _ => staged,
436 }
437}
438
439fn launch(spec: &WorkerSpec) -> Result<(std::process::Child, WorkerClient), DaemonError> {
440 use std::os::unix::io::AsRawFd;
441 use std::os::unix::process::CommandExt;
442
443 let (frames_a, frames_w) = UnixStream::pair()?;
444 let (fds_a, fds_w) = UnixStream::pair()?;
445 let frames_fd = frames_w.as_raw_fd();
446 let fdchan_fd = fds_w.as_raw_fd();
447 let mut cmd = std::process::Command::new(&spec.bin);
448 crate::exec::without_secrets(&mut cmd)
449 .args(&spec.args)
450 .arg("--frames-fd")
451 .arg("3")
452 .arg("--fd-channel-fd")
453 .arg("4");
454 if let Some(fd) = &spec.stderr {
455 cmd.stderr(std::process::Stdio::from(fd.try_clone()?));
456 cmd.stdout(std::process::Stdio::from(fd.try_clone()?));
457 }
458 unsafe {
464 cmd.pre_exec(move || {
465 let frames = libc::fcntl(frames_fd, libc::F_DUPFD_CLOEXEC, 10);
466 let fdchan = libc::fcntl(fdchan_fd, libc::F_DUPFD_CLOEXEC, 10);
467 if frames < 0 || fdchan < 0 || libc::dup2(frames, 3) < 0 || libc::dup2(fdchan, 4) < 0 {
468 return Err(std::io::Error::last_os_error());
469 }
470 Ok(())
471 });
472 }
473 let child = crate::exec::when_not_busy(|| cmd.spawn())?;
474 drop(frames_w);
475 drop(fds_w);
476 let client = WorkerClient::connect(frames_a, fds_a)?;
477 Ok((child, client))
478}
479
480impl Drop for EngineHost {
481 fn drop(&mut self) {
482 drop(self.client.take());
483 match &mut self.backend {
484 Backend::Thread(handle) => {
485 if let Some(h) = handle.take() {
486 let _ = h.join();
487 }
488 }
489 Backend::Process { child, .. } => {
490 let _ = child.wait();
491 }
492 }
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use std::sync::atomic::{AtomicBool, Ordering};
499 use std::sync::Arc;
500
501 use superfluid_engine::{EngineConfig, MockEngine};
502
503 use super::EngineHost;
504 use crate::DaemonError;
505
506 struct Unwound(Arc<AtomicBool>);
507 impl Drop for Unwound {
508 fn drop(&mut self) {
509 self.0.store(true, Ordering::SeqCst);
510 }
511 }
512
513 #[test]
514 fn try_spawn_refusal_is_engine_load_and_joins_the_worker() {
515 let unwound = Arc::new(AtomicBool::new(false));
516 let guard = Unwound(unwound.clone());
517 let err = EngineHost::try_spawn(move || {
518 let _guard = guard;
519 Err::<(MockEngine, _), _>("no paged KV on this backend".to_string())
520 })
521 .err()
522 .expect("refusal must surface as an error");
523 match err {
524 DaemonError::EngineLoad(msg) => assert_eq!(msg, "no paged KV on this backend"),
525 other => panic!("expected EngineLoad, got {other:?}"),
526 }
527 assert!(unwound.load(Ordering::SeqCst), "worker thread not joined");
528 }
529
530 #[test]
531 fn try_spawn_panicking_closure_reports_early_exit_without_panicking() {
532 let unwound = Arc::new(AtomicBool::new(false));
533 let guard = Unwound(unwound.clone());
534 let err = EngineHost::try_spawn(move || -> Result<(MockEngine, _), String> {
535 let _guard = guard;
536 panic!("engine build blew up");
537 })
538 .err()
539 .expect("a dead worker must surface as an error");
540 match err {
541 DaemonError::EngineLoad(msg) => {
542 assert_eq!(msg, "engine worker exited before reporting a load result")
543 }
544 other => panic!("expected EngineLoad, got {other:?}"),
545 }
546 assert!(unwound.load(Ordering::SeqCst), "worker thread not joined");
547 }
548
549 #[test]
550 fn try_spawn_success_builds_a_live_host() {
551 let host = EngineHost::try_spawn(|| Ok((MockEngine::new(EngineConfig::default()), None)))
552 .expect("mock engine loads");
553 assert!(!host.is_process());
554 drop(host);
555 }
556}