Skip to main content

vyre_runtime/
lib.rs

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