Skip to main content

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}