vyre_runtime/lib.rs
1//! Artifact execution, resident work queues, resource residency, and zero-copy IO.
2//!
3//! Runtime construction starts from an authenticated [`ArtifactSession`].
4//! Immutable compiler artifacts are materialized through registered target
5//! devices; runtime policy owns bindings, retained state, queueing, recovery,
6//! resource residency, IO, and telemetry.
7
8#![deny(missing_docs)]
9#![warn(unreachable_pub)]
10// vyre-runtime owns the io_uring zero-copy ingest path and the persistent
11// megakernel ring; both reach into FFI / mmap territory. Every unsafe site
12// carries a `Safety:` comment that `check_unsafe_justifications.sh` validates.
13#![allow(unsafe_code)]
14
15/// Errors surfaced by the runtime layer. Every variant carries a
16/// `Fix:`-bearing message so a reviewer can act on the failure.
17#[derive(Debug, Clone, thiserror::Error)]
18#[non_exhaustive]
19pub enum PipelineError {
20 /// Raw io_uring / libc syscall failed with an errno.
21 #[error("io_uring {syscall} failed: errno={errno}. Fix: {fix}")]
22 IoUringSyscall {
23 /// Which syscall failed (`io_uring_setup`, `mmap`, `io_uring_enter`).
24 syscall: &'static str,
25 /// Underlying errno value.
26 errno: i32,
27 /// Actionable remediation.
28 fix: &'static str,
29 },
30 /// io_uring submission or completion queue was full / overflowed.
31 #[error("io_uring {queue} queue at capacity. Fix: {fix}")]
32 QueueFull {
33 /// "submission" or "completion".
34 queue: &'static str,
35 /// Actionable remediation.
36 fix: &'static str,
37 },
38 /// Attempted to use io_uring on a non-Linux platform.
39 #[error(
40 "io_uring is Linux-only. Fix: run on Linux 5.1+ and attach an AsyncUringStream to UringCompletionPump"
41 )]
42 NotLinux,
43 /// Feature required for NVMe passthrough is not enabled.
44 #[error(
45 "NVMe passthrough requires the `uring-cmd-nvme` feature + Linux kernel 6.0+. Fix: add `features = [\"uring-cmd-nvme\"]` to your Cargo.toml"
46 )]
47 NvmePassthroughDisabled,
48 /// Backend error bubbled up from compile or dispatch.
49 #[error("backend error: {0}")]
50 Backend(String),
51 /// A megakernel dispatch ended before its work queue drained: only
52 /// `claimed` of `expected` `unit` were claimed, so the rest went unscanned
53 /// and this dispatch's hit set is INCOMPLETE, never a silent partial
54 /// (Law 10). A first-class variant (not a `Backend` string) so callers such
55 /// as the `seg_len` calibrator can EXCLUDE a too-fine geometry by matching
56 /// the type, never by substring-scanning the message text.
57 #[error(
58 "{descriptor} drain incomplete: only {claimed} of {expected} {unit} were claimed before \
59 the dispatch ended, so {unscanned} {unit} went unscanned and their matches were dropped. \
60 This dispatch's hit set is INCOMPLETE. Fix: raise the dispatch timeout \
61 (BatchDispatchConfig.timeout) so the drain loop can exhaust the queue, or shard the batch \
62 into smaller queues.",
63 unscanned = expected.saturating_sub(*claimed),
64 )]
65 DrainIncomplete {
66 /// Which dispatch path under-drained: `"megakernel"` (per-rule) or
67 /// `"combined megakernel"` (combined-AC). Names the failing path in the
68 /// operator message without a second string variant.
69 descriptor: &'static str,
70 /// Work-items/segments actually claimed before the dispatch ended.
71 claimed: u32,
72 /// Work-items/segments that should have been claimed (full queue length).
73 expected: u32,
74 /// The unit being drained: `"work-items"` (per-rule) or `"segments"`
75 /// (combined-AC). Interpolated twice for a grammatical message.
76 unit: &'static str,
77 },
78}
79
80impl PipelineError {
81 /// True iff this is a [`PipelineError::DrainIncomplete`]: a dispatch that
82 /// could not exhaust its work queue within the timeout.
83 ///
84 /// Distinct from a hard backend failure, the `seg_len` calibrator
85 /// EXCLUDES a geometry that drains incompletely (too fine to drain in the
86 /// configured timeout) rather than aborting the whole calibration, while it
87 /// must still PROPAGATE any other [`PipelineError`]. Match on this predicate
88 /// instead of substring-scanning the Display message, which is fragile to
89 /// wording changes.
90 #[must_use]
91 pub fn is_drain_incomplete(&self) -> bool {
92 matches!(self, Self::DrainIncomplete { .. })
93 }
94}
95
96impl From<vyre_driver::backend::BackendError> for PipelineError {
97 fn from(err: vyre_driver::backend::BackendError) -> Self {
98 PipelineError::Backend(err.to_string())
99 }
100}
101
102/// Canonical artifact-envelope authentication and exact-format admission.
103pub mod artifact_admission;
104
105/// Backend-neutral immutable-resource and mutable-state residency.
106pub mod resource_residency;
107
108/// Resident work-queue protocols, scheduling policy, and runtime IO.
109#[path = "megakernel/mod.rs"]
110pub mod resident_work_queue;
111
112/// Authenticated persistent execution over retained artifact bindings.
113pub mod persistent_executor;
114/// Content-addressed authenticated artifact cache.
115pub mod pipeline_cache;
116
117/// Structured artifact-session recovery without message parsing or recompilation.
118pub mod recovery;
119/// Differential megakernel replay log - captures every published
120/// ring slot so a later cert run can diff epoch-by-epoch execution
121/// against a live backend.
122pub mod replay;
123
124/// Backend routing policy for execution plans.
125pub mod routing;
126
127/// Multi-GPU work partitioning across runtime backends.
128pub mod scheduler;
129
130/// Multi-tenant megakernel multiplexing - one persistent kernel per
131/// GPU, shared across producer tools via the `tenant_id` field already
132/// in the ring protocol.
133pub mod tenant;
134
135pub use replay::{
136 RecordedSlot, ReplayFailureClass, ReplayFailureEvidence, ReplayLogError, ReplayRecord, RingLog,
137};
138pub use tenant::{
139 TenantError, TenantHandle, TenantRegistry, OPCODE_RANGE_PER_TENANT, TENANT_ID_MAX,
140 TENANT_OPCODE_BASE,
141};
142
143#[cfg(feature = "remote-cache")]
144pub use pipeline_cache::RemoteCache;
145pub use pipeline_cache::{
146 DiskCache, DiskCacheError, InMemoryPipelineCache, LayeredPipelineCache,
147 PipelineCacheMetricError, PipelineCacheMetrics, PipelineCacheStore, PipelineFingerprint,
148};
149
150pub use artifact_admission::{
151 admit_artifact, admit_cached_artifact, admit_envelope, AdmittedArtifact,
152 ArtifactAdmissionError, ArtifactSession, ArtifactSessionError, RetainedArtifactSession,
153};
154pub use persistent_executor::{PersistentExecutor, ResidentQueueCompletion, ResidentQueueState};
155pub use recovery::{classify_backend_error, recover_artifact_session};
156pub use vyre_foundation::diagnostics::RetryClass;
157
158/// Linux io_uring integration. Compiled out on macOS / Windows.
159#[cfg(target_os = "linux")]
160#[allow(unsafe_code)]
161pub mod uring;
162
163/// Completion pump for an optional Linux io_uring stream.
164///
165/// Detached pumps report [`UringPollState::Detached`] instead of fabricating a
166/// zero-completion observation.
167pub struct UringCompletionPump<'a> {
168 #[cfg(target_os = "linux")]
169 uring: Option<uring::AsyncUringStream<'a>>,
170 // On macOS / Windows the `uring` field is compiled out, which leaves the
171 // `'a` lifetime unused and the compiler rejects the struct. Carry a
172 // zero-sized marker so the lifetime stays live on non-Linux targets.
173 #[cfg(not(target_os = "linux"))]
174 _phantom: std::marker::PhantomData<&'a ()>,
175 shutdown_requested: bool,
176}
177
178impl Default for UringCompletionPump<'_> {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184/// Result of one non-blocking completion-pump probe.
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub enum UringPollState {
187 /// No io_uring stream is attached.
188 Detached,
189 /// An attached stream was polled and produced this many completions.
190 Completed(u32),
191}
192
193impl<'a> UringCompletionPump<'a> {
194 /// Create a pipeline handle with no io_uring stream attached.
195 ///
196 /// # Examples
197 ///
198 /// ```
199 /// use vyre_runtime::UringCompletionPump;
200 ///
201 /// let pump = UringCompletionPump::new();
202 ///
203 /// assert!(!pump.is_shutdown_requested());
204 /// ```
205 #[must_use]
206 pub fn new() -> Self {
207 Self {
208 #[cfg(target_os = "linux")]
209 uring: None,
210 #[cfg(not(target_os = "linux"))]
211 _phantom: std::marker::PhantomData,
212 shutdown_requested: false,
213 }
214 }
215
216 /// Attach an io_uring stream for GPU-visible reads. Linux-only.
217 ///
218 /// Use `uring::NvmeGpuIngestDriver::new_gpudirect` when the caller
219 /// requires the native NVMe → BAR1 path instead of registered mapped reads.
220 #[cfg(target_os = "linux")]
221 #[must_use]
222 pub fn with_uring(mut self, stream: uring::AsyncUringStream<'a>) -> Self {
223 self.uring = Some(stream);
224 self
225 }
226
227 /// Probe the attached io_uring stream for completions.
228 ///
229 /// # Errors
230 ///
231 /// Propagates any uring syscall error from the underlying ring.
232 pub fn poll(&mut self) -> Result<UringPollState, PipelineError> {
233 #[cfg(target_os = "linux")]
234 {
235 if let Some(ref mut stream) = self.uring {
236 return stream.poll().map(UringPollState::Completed);
237 }
238 }
239 Ok(UringPollState::Detached)
240 }
241
242 /// Request graceful shutdown of the pipeline.
243 pub fn request_shutdown(&mut self) {
244 self.shutdown_requested = true;
245 }
246
247 /// Whether shutdown has been requested.
248 #[must_use]
249 pub fn is_shutdown_requested(&self) -> bool {
250 self.shutdown_requested
251 }
252
253 /// Block until the megakernel writes a new value into the
254 /// observable word. Uses `futex_waitv` on Linux 5.16+.
255 ///
256 /// # Errors
257 ///
258 /// - [`PipelineError::NotLinux`] on non-Linux hosts.
259 /// - [`PipelineError::IoUringSyscall`] on futex errors.
260 ///
261 /// # Safety
262 ///
263 /// `host_visible_addr` must be host-mapped and outlive this call.
264 #[cfg(target_os = "linux")]
265 #[allow(unsafe_code)]
266 pub unsafe fn wait_for_observable(
267 host_visible_addr: *const u32,
268 current: u32,
269 timeout_ns: u64,
270 ) -> Result<(), PipelineError> {
271 #[repr(C)]
272 struct futex_waitv {
273 val: u64,
274 uaddr: u64,
275 flags: u32,
276 __reserved: u32,
277 }
278 const FUTEX2_SIZE_U32: u32 = 0x02;
279 const SYS_FUTEX_WAITV: libc::c_long = 449;
280
281 let waitv = [futex_waitv {
282 val: current as u64,
283 uaddr: host_visible_addr as u64,
284 flags: FUTEX2_SIZE_U32,
285 __reserved: 0,
286 }];
287
288 #[repr(C)]
289 struct Timespec {
290 tv_sec: i64,
291 tv_nsec: i64,
292 }
293 let ts = Timespec {
294 tv_sec: (timeout_ns / 1_000_000_000) as i64,
295 tv_nsec: (timeout_ns % 1_000_000_000) as i64,
296 };
297
298 // SAFETY: Safe FFI / low-level operation verified and audited for Release compliance.
299 let res = unsafe {
300 libc::syscall(
301 SYS_FUTEX_WAITV,
302 waitv.as_ptr() as *const libc::c_void,
303 1u32,
304 0u32,
305 &ts as *const Timespec,
306 0u64,
307 )
308 };
309
310 if res < 0 {
311 // SAFETY: Safe FFI / low-level operation verified and audited for Release compliance.
312 let errno = unsafe { *libc::__errno_location() };
313 if errno == libc::EAGAIN {
314 return Ok(());
315 }
316 return Err(PipelineError::IoUringSyscall {
317 syscall: "futex_waitv",
318 errno,
319 fix: "kernel 5.16+ required; ETIMEDOUT means the value didn't change within timeout_ns",
320 });
321 }
322 Ok(())
323 }
324
325 /// Non-Linux implementation returning the structured platform error.
326 #[cfg(not(target_os = "linux"))]
327 #[allow(unsafe_code, clippy::missing_safety_doc)]
328 pub unsafe fn wait_for_observable(
329 _host_visible_addr: *const u32,
330 _current: u32,
331 _timeout_ns: u64,
332 ) -> Result<(), PipelineError> {
333 Err(PipelineError::NotLinux)
334 }
335}
336
337/// Linux-only: host-visible GPU buffer that io_uring can DMA into.
338#[cfg(target_os = "linux")]
339pub use uring::GpuMappedBuffer;
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn construct_stream_has_no_shutdown() {
347 let stream = UringCompletionPump::new();
348 assert!(!stream.is_shutdown_requested());
349 }
350
351 #[test]
352 fn shutdown_is_idempotent() {
353 let mut stream = UringCompletionPump::new();
354 stream.request_shutdown();
355 stream.request_shutdown();
356 assert!(stream.is_shutdown_requested());
357 }
358
359 #[test]
360 fn poll_without_uring_reports_detached_state() {
361 let mut stream = UringCompletionPump::new();
362 assert_eq!(stream.poll().unwrap(), UringPollState::Detached);
363 }
364
365 #[test]
366 fn drain_incomplete_is_distinguishable_by_type_not_substring() {
367 // Regression: the seg_len calibrator must EXCLUDE a too-fine geometry
368 // (drain-incomplete) but PROPAGATE every other backend failure. It used
369 // to discriminate by `to_string().contains("drain incomplete")`, which
370 // silently turns into "abort the whole calibration" the moment the
371 // message wording drifts. The structured variant + predicate is the
372 // contract; this test pins it.
373 let drain = PipelineError::DrainIncomplete {
374 descriptor: "combined megakernel",
375 claimed: 3,
376 expected: 10,
377 unit: "segments",
378 };
379 assert!(drain.is_drain_incomplete());
380
381 // The Display message stays operator-actionable AND keeps the
382 // "drain incomplete" phrase + computed unscanned count, so legacy
383 // substring matchers and operator logs do not regress.
384 let msg = drain.to_string();
385 assert_eq!(
386 msg,
387 "combined megakernel drain incomplete: only 3 of 10 segments were claimed before the \
388 dispatch ended, so 7 segments went unscanned and their matches were dropped. This \
389 dispatch's hit set is INCOMPLETE. Fix: raise the dispatch timeout \
390 (BatchDispatchConfig.timeout) so the drain loop can exhaust the queue, or shard the \
391 batch into smaller queues."
392 );
393
394 // The per-rule path uses the same variant with a different descriptor/unit.
395 let per_rule = PipelineError::DrainIncomplete {
396 descriptor: "megakernel",
397 claimed: 0,
398 expected: 4,
399 unit: "work-items",
400 };
401 assert!(per_rule.is_drain_incomplete());
402 assert!(
403 per_rule
404 .to_string()
405 .starts_with("megakernel drain incomplete: only 0 of 4 work-items were claimed"),
406 "msg was: {}",
407 per_rule
408 );
409
410 // A genuine backend failure is NOT a drain-incomplete: it must surface
411 // as a hard error, never be excluded-and-continued by the calibrator.
412 let backend = PipelineError::Backend("adapter lost".to_string());
413 assert!(!backend.is_drain_incomplete());
414 }
415}