Skip to main content

vyre_driver_wgpu/engine/
streaming.rs

1//! Host-ingress compatibility stream for chunked inputs.
2//!
3//! This module is not VYRE's canonical streaming model. It exists for callers
4//! that still receive bytes through host memory and need a bounded bridge while
5//! the device-resident megakernel queue is being used elsewhere. The stream
6//! owns a compiled `WgpuPipeline` and keeps at most one chunk in flight.
7//! Calling `HostIngressStream::push_chunk` starts GPU work for the new chunk,
8//! then returns the previous chunk's completed output.
9//!
10//! The CPU side is limited to ingress orchestration: owning the worker queue,
11//! handing byte chunks to wgpu, and collecting completion. It must not perform
12//! parser, matcher, scheduler, retry, or analysis semantics. The canonical
13//! VYRE path is `vyre-runtime::megakernel`: CPU launches/publishes descriptors,
14//! while the GPU owns phase progression and execution.
15//!
16//! Worker-pool channel: `crossbeam-channel`. `std::sync::mpsc::Receiver` is
17//! single-consumer  -  wrapping it in `Arc<Mutex<_>>` to let N workers drain
18//! the same queue serialises wakeups on the mutex (the audit called this
19//! "Mutex<mpsc::Receiver> locking every recv"). crossbeam-channel is
20//! multi-producer multi-consumer natively, so N workers do N independent
21//! lock-free recvs.
22
23use std::sync::{Arc, LazyLock};
24
25use crossbeam_channel::{bounded, Receiver, Sender};
26use vyre_driver::{BackendError, CompiledPipeline, DispatchConfig};
27
28use crate::pipeline::WgpuPipeline;
29use crate::thread_pool::{BoundedWorkerJob, BoundedWorkerPool};
30
31/// Async copy stream primitives.
32pub mod async_copy;
33
34/// Host-ingress adapter for one in-flight chunked dispatch stream.
35///
36/// This is a compatibility adapter for environments where input bytes arrive
37/// through host memory. It is intentionally named after ingress, not execution:
38/// the device-resident execution model lives in `vyre-runtime::megakernel`.
39pub struct HostIngressStream {
40    runner:
41        Arc<dyn Fn(Vec<u8>, DispatchConfig) -> Result<Vec<Vec<u8>>, BackendError> + Send + Sync>,
42    config: DispatchConfig,
43    in_flight: Option<Receiver<Result<Vec<Vec<u8>>, BackendError>>>,
44}
45
46type ChunkResult = Result<Vec<Vec<u8>>, BackendError>;
47
48struct ChunkJob {
49    runner:
50        Arc<dyn Fn(Vec<u8>, DispatchConfig) -> Result<Vec<Vec<u8>>, BackendError> + Send + Sync>,
51    bytes: Vec<u8>,
52    config: DispatchConfig,
53    response: Sender<ChunkResult>,
54}
55
56struct StreamingPool {
57    pool: BoundedWorkerPool<ChunkJob>,
58}
59
60impl StreamingPool {
61    fn global() -> Result<&'static Self, BackendError> {
62        static POOL: LazyLock<Result<StreamingPool, BackendError>> =
63            LazyLock::new(StreamingPool::new);
64        POOL.as_ref().map_err(|e| BackendError::new(e.to_string()))
65    }
66
67    fn new() -> Result<Self, BackendError> {
68        const JOB_QUEUE: usize = 64;
69        Ok(Self {
70            pool: BoundedWorkerPool::new(
71                JOB_QUEUE,
72                "vyre-wgpu-streaming",
73                "inspect the chunk program and GPU driver logs.",
74                "reduce process thread count or increase system nproc limit.",
75            )?,
76        })
77    }
78
79    fn submit(
80        &self,
81        runner: Arc<
82            dyn Fn(Vec<u8>, DispatchConfig) -> Result<Vec<Vec<u8>>, BackendError> + Send + Sync,
83        >,
84        bytes: Vec<u8>,
85        config: DispatchConfig,
86    ) -> Result<Receiver<ChunkResult>, BackendError> {
87        let (sender, receiver) = bounded(1);
88        let job = ChunkJob {
89            runner,
90            bytes,
91            config,
92            response: sender,
93        };
94        self.pool.submit_blocking(
95            job,
96            "recreate the process; the global stream pool only closes during shutdown.",
97        )?;
98        Ok(receiver)
99    }
100}
101
102impl BoundedWorkerJob for ChunkJob {
103    type Output = Vec<Vec<u8>>;
104
105    fn response(&self) -> &Sender<ChunkResult> {
106        &self.response
107    }
108
109    fn run(self) -> ChunkResult {
110        (self.runner)(self.bytes, self.config)
111    }
112}
113
114impl HostIngressStream {
115    /// Create a host-ingress stream from a compiled wgpu pipeline.
116    #[must_use]
117    pub fn new(pipeline: WgpuPipeline, config: DispatchConfig) -> Self {
118        let runner = Arc::new(move |bytes: Vec<u8>, config: DispatchConfig| {
119            pipeline.dispatch(&[bytes], &config)
120        });
121        Self {
122            runner,
123            config,
124            in_flight: None,
125        }
126    }
127
128    /// Create a host-ingress stream from a custom chunk runner.
129    #[must_use]
130    pub fn from_runner<F>(runner: F, config: DispatchConfig) -> Self
131    where
132        F: Fn(Vec<u8>, DispatchConfig) -> Result<Vec<Vec<u8>>, BackendError>
133            + Send
134            + Sync
135            + 'static,
136    {
137        Self {
138            runner: Arc::new(runner),
139            config,
140            in_flight: None,
141        }
142    }
143
144    /// Push a host-memory chunk and return the previous chunk's output when
145    /// one exists.
146    ///
147    /// # Errors
148    ///
149    /// Returns a backend error if the previous chunk failed or the worker
150    /// thread panicked before reporting a backend result.
151    pub fn push_chunk(&mut self, bytes: Vec<u8>) -> Result<Option<Vec<Vec<u8>>>, BackendError> {
152        let previous = self.take_finished()?;
153        let runner = Arc::clone(&self.runner);
154        let config = self.config.clone();
155        self.in_flight = Some(StreamingPool::global()?.submit(runner, bytes, config)?);
156        Ok(previous)
157    }
158
159    /// Wait for the final in-flight chunk and return its output.
160    ///
161    /// # Errors
162    ///
163    /// Returns a backend error if the final chunk failed or the worker panicked.
164    pub fn finish(&mut self) -> Result<Option<Vec<Vec<u8>>>, BackendError> {
165        self.take_finished()
166    }
167
168    fn take_finished(&mut self) -> Result<Option<Vec<Vec<u8>>>, BackendError> {
169        let Some(handle) = self.in_flight.take() else {
170            return Ok(None);
171        };
172        handle.recv().map_err(|error| {
173            BackendError::new(
174                format!("host-ingress worker ended before sending a result: {error}. Fix: inspect worker-pool lifecycle and GPU driver logs."),
175            )
176        })?
177        .map(Some)
178    }
179}