Skip to main content

hrx/execution/
native_session.rs

1use super::{GpuLane, Runtime, scheduler::NativeLease};
2use crate::{Error, Result};
3use std::{
4    cell::RefCell,
5    panic::{AssertUnwindSafe, catch_unwind, resume_unwind},
6    sync::Arc,
7};
8
9thread_local! {
10    static ACTIVE: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
11}
12
13struct Entered(usize);
14impl Entered {
15    fn new(runtime: &Runtime) -> Result<Self> {
16        let id = Arc::as_ptr(&runtime.inner.core) as usize;
17        ACTIVE.with(|active| {
18            let mut active = active.borrow_mut();
19            if active.contains(&id) {
20                return Err(Error::Busy(
21                    "recursive native session in the same runtime".into(),
22                ));
23            }
24            active.push(id);
25            Ok(Self(id))
26        })
27    }
28}
29impl Drop for Entered {
30    fn drop(&mut self) {
31        ACTIVE.with(|active| active.borrow_mut().retain(|id| *id != self.0));
32    }
33}
34
35/// Owned stream-bound model state scheduled alongside prepared GPU graphs.
36///
37/// Stages execute synchronously on the calling thread and may borrow inputs and
38/// progress callbacks. They reserve the compute lane, without blocking unrelated
39/// upload/download/NPU lanes. Earlier submissions drain before admission; later
40/// compute cannot overtake a waiting stage. Admission uses the runtime's bounded
41/// submission capacity. This is a stage boundary, not asynchronous inference or
42/// separate transfer scheduling within a stage.
43pub struct NativeSession<T> {
44    runtime: Runtime,
45    state: Option<T>,
46    fence: fn(&mut T) -> Result<()>,
47}
48
49impl<T> NativeSession<T> {
50    /// Own a model's private native state and its completion fence.
51    ///
52    /// # Safety
53    /// State must own every private stream, graph and allocation used by its
54    /// stages, on this runtime's selected device. `fence` must drain all those
55    /// streams before returning success, including after a failed stage. No
56    /// concurrent external access to this state is allowed. Construction must
57    /// leave no pending work; subsequent work must go through [`Self::run`].
58    pub unsafe fn new(runtime: &Runtime, state: T, fence: fn(&mut T) -> Result<()>) -> Self {
59        Self {
60            runtime: runtime.clone(),
61            state: Some(state),
62            fence,
63        }
64    }
65
66    /// Inspect idle model metadata. Returns `DeviceLost` after quarantine.
67    pub fn state(&self) -> Result<&T> {
68        self.state
69            .as_ref()
70            .ok_or_else(|| Error::DeviceLost("native session is quarantined".into()))
71    }
72
73    /// Reserve compute, run a borrowed stage, then fence on every exit path.
74    /// Client errors may be returned as `R = Result<_, ClientError>` and do not
75    /// poison a successfully drained session. A panic is resumed after fencing.
76    /// A failed or panicking fence quarantines the entire owner, including its
77    /// allocation charges, and rejects all subsequent stages.
78    ///
79    /// # Safety
80    /// The stage must retain all resources until the fence, submit only on the
81    /// owned streams, and not expose pending raw uses outside this session.
82    /// It must not transfer private native owners out of the state, including
83    /// through its return value; quarantine must retain the complete owner.
84    /// Stages may access only private native storage, not coordinated buffers;
85    /// use `Graph::gpu_scoped` for operations needing tracked memory hazards.
86    /// Do not wait for coordinated compute or another native session in this
87    /// runtime from the callback: this stage already holds its compute lane.
88    pub unsafe fn run<R>(&mut self, stage: impl FnOnce(&mut T) -> R) -> Result<R> {
89        self.state()?;
90        let _entered = Entered::new(&self.runtime)?;
91        let _lane = NativeLease::acquire(&self.runtime.inner.core)?;
92        let started = std::time::Instant::now();
93        let state = self.state.as_mut().expect("checked before admission");
94        let outcome = catch_unwind(AssertUnwindSafe(|| stage(state)));
95        let completion = catch_unwind(AssertUnwindSafe(|| (self.fence)(state)));
96        let failed = !matches!(completion, Ok(Ok(())));
97        self.runtime.inner.core.tracer.record(
98            started,
99            started.elapsed(),
100            GpuLane::Compute as usize,
101            0,
102            failed || outcome.is_err(),
103        );
104        if failed {
105            // Keep graphs, stream storage, checkpoint mappings and charges alive
106            // beneath possibly in-flight native work. Never retry this owner.
107            std::mem::forget(self.state.take());
108        }
109        match outcome {
110            Err(panic) => resume_unwind(panic),
111            Ok(value) => match completion {
112                Err(panic) => resume_unwind(panic),
113                Ok(result) => result.map(|()| value),
114            },
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use std::sync::atomic::{AtomicUsize, Ordering};
123
124    struct State {
125        fences: Arc<AtomicUsize>,
126        drops: Arc<AtomicUsize>,
127        fail: bool,
128        panic: bool,
129    }
130    impl Drop for State {
131        fn drop(&mut self) {
132            self.drops.fetch_add(1, Ordering::SeqCst);
133        }
134    }
135    fn session(runtime: &Runtime) -> NativeSession<State> {
136        // SAFETY: mock state performs no device work.
137        unsafe {
138            NativeSession::new(
139                runtime,
140                State {
141                    fences: Arc::new(AtomicUsize::new(0)),
142                    drops: Arc::new(AtomicUsize::new(0)),
143                    fail: false,
144                    panic: false,
145                },
146                |state| {
147                    state.fences.fetch_add(1, Ordering::SeqCst);
148                    assert!(!state.panic, "fence panic");
149                    if state.fail {
150                        Err(Error::DeviceLost("fence failed".into()))
151                    } else {
152                        Ok(())
153                    }
154                },
155            )
156        }
157    }
158
159    #[test]
160    fn borrowed_stages_drain_errors_and_panics_then_reuse() {
161        let runtime = Runtime::new().unwrap();
162        let mut session = session(&runtime);
163        let drops = session.state().unwrap().drops.clone();
164        let mut output = 0;
165        // SAFETY: all test callbacks only mutate host state.
166        unsafe {
167            session.run(|_| {
168                output = 42;
169                Err::<(), _>("client error")
170            })
171        }
172        .unwrap()
173        .unwrap_err();
174        assert_eq!(output, 42);
175        assert!(
176            catch_unwind(AssertUnwindSafe(|| unsafe {
177                session.run::<()>(|_| panic!("stage panic"))
178            }))
179            .is_err()
180        );
181        assert_eq!(unsafe { session.run(|_| 7) }.unwrap(), 7);
182        assert_eq!(session.state().unwrap().fences.load(Ordering::SeqCst), 3);
183        drop(session);
184        assert_eq!(drops.load(Ordering::SeqCst), 1);
185    }
186
187    #[test]
188    #[cfg_attr(miri, ignore = "deliberate quarantine leak")]
189    fn uncertain_fences_keep_owners_and_reject_retry() {
190        for panic in [false, true] {
191            let runtime = Runtime::new().unwrap();
192            let mut session = session(&runtime);
193            let drops = session.state().unwrap().drops.clone();
194            let fences = session.state().unwrap().fences.clone();
195            let result = catch_unwind(AssertUnwindSafe(|| unsafe {
196                session.run(|state| {
197                    state.fail = true;
198                    state.panic = panic;
199                })
200            }));
201            if panic {
202                assert!(result.is_err());
203            } else {
204                assert!(result.unwrap().is_err());
205            }
206            assert!(matches!(
207                unsafe { session.run::<()>(|_| unreachable!()) },
208                Err(Error::DeviceLost(_))
209            ));
210            drop(session);
211            assert_eq!(fences.load(Ordering::SeqCst), 1);
212            assert_eq!(drops.load(Ordering::SeqCst), 0);
213            // The failed owner is quarantined, not the entire runtime lane.
214            let mut next = super::tests::session(&runtime);
215            unsafe { next.run(|_| ()) }.unwrap();
216        }
217    }
218
219    #[test]
220    fn recursive_admission_returns_busy_instead_of_deadlocking() {
221        let runtime = Runtime::new().unwrap();
222        let mut outer = session(&runtime);
223        let mut inner = session(&runtime);
224        let result = unsafe { outer.run(|_| inner.run(|_| ())) }.unwrap();
225        assert!(matches!(result, Err(Error::Busy(_))));
226        unsafe { inner.run(|_| ()) }.unwrap();
227    }
228}