Skip to main content

eredu_core/backend/
continuation.rs

1//! Detached continuations for the ordinary text-generation machine.
2
3use super::*;
4use std::sync::Arc;
5
6/// Unforgeable identity of one continuation, distinct from its shared driver.
7#[derive(Clone)]
8pub struct TextContinuationIdentity(Arc<()>);
9
10impl PartialEq for TextContinuationIdentity {
11    fn eq(&self, other: &Self) -> bool {
12        Arc::ptr_eq(&self.0, &other.0)
13    }
14}
15impl Eq for TextContinuationIdentity {}
16
17/// Unforgeable identity of the exclusive driver shared by one serial branch tree.
18/// A later generation on the same loaded executable has a different identity.
19#[derive(Clone)]
20pub struct TextDriverIdentity(Arc<()>);
21impl PartialEq for TextDriverIdentity {
22    fn eq(&self, other: &Self) -> bool {
23        Arc::ptr_eq(&self.0, &other.0)
24    }
25}
26impl Eq for TextDriverIdentity {}
27
28/// Failure to advance or settle a detached ordinary continuation.
29#[derive(Debug, thiserror::Error)]
30pub enum TextContinuationError<B, C>
31where
32    B: std::error::Error + 'static,
33    C: std::error::Error + 'static,
34{
35    /// The continuation was created by another exclusive runtime driver.
36    #[error("text continuation belongs to a different runtime driver")]
37    IncompatibleDriver,
38    /// A previous operation failed; only cleanup and record draining remain valid.
39    #[error("text continuation failed and cannot advance")]
40    Failed,
41    /// Native completion and portable record draining have not both succeeded.
42    #[error("text continuation has no completed, drained boundary")]
43    NotQuiescent,
44    /// Ordinary backend execution or portable constraint control failed.
45    #[error(transparent)]
46    Generation(#[from] ControlledTextGenerationError<B, C>),
47}
48
49/// Pending input, sampling state and constraint state of the ordinary machine.
50///
51/// This is neither a full generation snapshot nor a cloneable native state.
52/// After commitment, the last token remains pending decode input. Completion
53/// handles stay here until settling or drop. The owning facade also retains
54/// native model state, semantic decoding, termination and output delivery.
55pub struct TextGenerationContinuation<B, C>
56where
57    B: TextGenerationBackend,
58    C: TokenFilterController,
59{
60    owner: Arc<()>,
61    identity: TextContinuationIdentity,
62    inner: TextGenerationMachine<B, C>,
63    failed: bool,
64    records_drained: bool,
65}
66
67impl<B, C> TextGenerationContinuation<B, C>
68where
69    B: TextGenerationBackend,
70    C: TokenFilterController,
71{
72    /// Borrows the canonical constraint state without advancing it.
73    pub fn controller(&self) -> &C {
74        &self.inner.controller
75    }
76
77    /// Borrows the canonical constraint state for the ordinary semantic checks.
78    pub fn controller_mut(&mut self) -> &mut C {
79        &mut self.inner.controller
80    }
81
82    /// Remaining ordinary token allowance, independent of facade EOS policy.
83    pub fn remaining_tokens(&self) -> Option<usize> {
84        self.inner.remaining_tokens
85    }
86
87    /// Whether the next model input is the original prompt rather than a token.
88    pub fn is_prefill_pending(&self) -> bool {
89        matches!(self.inner.step, Some(PendingTextInput::Prefill(_)))
90    }
91
92    /// Requires exact completion and delivery before snapshot composition.
93    /// This does not establish the facade's semantic or lifecycle boundary.
94    pub fn require_quiescent(&self) -> Result<(), TextContinuationError<B::Error, C::Error>> {
95        if self.failed {
96            return Err(TextContinuationError::Failed);
97        }
98        if !self.inner.completions.is_empty() || !self.records_drained {
99            return Err(TextContinuationError::NotQuiescent);
100        }
101        Ok(())
102    }
103}
104
105/// Exclusive execution owner for detached ordinary continuations.
106///
107/// The same machine drives the existing borrowed iterators. This owner allows a
108/// facade to retain separate continuations while serially switching independently
109/// saved native model states. It does not switch those native states itself: the
110/// facade must compose the backend's validated state-exchange mechanism. Native
111/// objects need not implement `Send` or `Sync`.
112pub struct TextGenerationDriver<'a, B: TextGenerationBackend> {
113    runtime: &'a mut ModelRuntime<B>,
114    owner: Arc<()>,
115}
116
117impl<'a, B: TextGenerationBackend> TextGenerationDriver<'a, B> {
118    /// Exclusively borrows the runtime while this driver exists. A continuation
119    /// retained after driver drop can be cleaned up but cannot attach to another
120    /// driver, even one borrowing the same runtime.
121    pub fn new(runtime: &'a mut ModelRuntime<B>) -> Self {
122        Self {
123            runtime,
124            owner: Arc::new(()),
125        }
126    }
127
128    /// Read-only access for exact loaded-session discovery and native estimates.
129    pub fn runtime(&self) -> &ModelRuntime<B> {
130        self.runtime
131    }
132
133    /// Starts the existing ordinary machine with an opaque prepared prompt.
134    /// Neither model execution nor token sampling occurs here.
135    pub fn start<C: TokenFilterController>(
136        &mut self,
137        prompt: B::Prompt,
138        config: TextGenerationConfig,
139        controller: C,
140    ) -> Result<TextGenerationContinuation<B, C>, ControlledTextGenerationError<B::Error, C::Error>>
141    {
142        Ok(TextGenerationContinuation {
143            owner: Arc::clone(&self.owner),
144            identity: TextContinuationIdentity(Arc::new(())),
145            inner: TextGenerationMachine::new(self.runtime, prompt, config, controller)?,
146            failed: false,
147            records_drained: true,
148        })
149    }
150
151    fn validate<C: TokenFilterController>(
152        &self,
153        state: &TextGenerationContinuation<B, C>,
154    ) -> Result<(), TextContinuationError<B::Error, C::Error>> {
155        if !Arc::ptr_eq(&self.owner, &state.owner) {
156            return Err(TextContinuationError::IncompatibleDriver);
157        }
158        Ok(())
159    }
160
161    /// Lends the completed ordinary state to portable snapshot composition.
162    /// All native work and the preceding bounded record batch must be settled.
163    /// The guard holds both mutable borrows, preventing advancement while native
164    /// copies, compatibility validation and host-state installation are composed.
165    pub fn quiescent<'d, 's, C: TokenFilterController>(
166        &'d mut self,
167        state: &'s mut TextGenerationContinuation<B, C>,
168    ) -> Result<TextContinuationBoundary<'d, 's, B, C>, TextContinuationError<B::Error, C::Error>>
169    {
170        self.validate(state)?;
171        state.require_quiescent()?;
172        Ok(TextContinuationBoundary {
173            runtime: self.runtime,
174            state,
175        })
176    }
177
178    /// Advances at most one constraint-committed canonical token using the
179    /// ordinary sampler and pending input. Call `take_completed_step` before
180    /// advancing again or composing a pause/snapshot boundary.
181    #[allow(clippy::type_complexity)]
182    pub fn advance<C: TokenFilterController>(
183        &mut self,
184        state: &mut TextGenerationContinuation<B, C>,
185    ) -> Result<Option<ControlledToken<B::Token>>, TextContinuationError<B::Error, C::Error>> {
186        self.validate(state)?;
187        state.require_quiescent()?;
188        // A caught unwind must not make a partially advanced machine resumable.
189        state.failed = true;
190        state.records_drained = false;
191        match state.inner.next_committed(self.runtime) {
192            None => {
193                state.failed = false;
194                state.records_drained = true;
195                Ok(None)
196            }
197            Some(Ok(token)) => {
198                state.failed = false;
199                Ok(Some(token))
200            }
201            Some(Err(error)) => Err(TextContinuationError::Generation(error)),
202        }
203    }
204
205    /// Settles exact native completion and moves out the single bounded record
206    /// batch. It remains available after failure for attributed error evidence;
207    /// draining never turns a failed continuation into a resumable one.
208    pub fn take_completed_step<C: TokenFilterController>(
209        &mut self,
210        state: &mut TextGenerationContinuation<B, C>,
211    ) -> Result<Option<crate::capture::CapturedStep>, TextContinuationError<B::Error, C::Error>>
212    {
213        self.validate(state)?;
214        let was_failed = state.failed;
215        state.failed = true;
216        if let Err(error) = state.inner.resolve_completions_before_decode() {
217            return Err(ControlledTextGenerationError::Backend(error).into());
218        }
219        let records = B::take_text_capture(&mut state.inner.backend_state);
220        state.records_drained = true;
221        state.failed = was_failed;
222        Ok(records)
223    }
224
225    /// Installs observations before the first model prediction.
226    pub fn enable_capture<C: TokenFilterController>(
227        &mut self,
228        state: &mut TextGenerationContinuation<B, C>,
229        plan: crate::capture::AdmittedCapturePlan,
230    ) -> Result<(), crate::capture::CaptureError> {
231        self.validate_installation(state)?;
232        B::configure_text_capture(self.runtime, &mut state.inner.backend_state, plan)
233    }
234
235    /// Installs immutable observation/intervention admissions before execution.
236    pub fn enable_interventions<C: TokenFilterController>(
237        &mut self,
238        state: &mut TextGenerationContinuation<B, C>,
239        capture: crate::capture::AdmittedCapturePlan,
240        plan: crate::intervention::AdmittedInterventionPlan,
241    ) -> Result<(), crate::capture::CaptureError> {
242        self.validate_installation(state)?;
243        B::configure_text_interventions(self.runtime, &mut state.inner.backend_state, capture, plan)
244    }
245
246    fn validate_installation<C: TokenFilterController>(
247        &self,
248        state: &TextGenerationContinuation<B, C>,
249    ) -> Result<(), crate::capture::CaptureError> {
250        self.validate(state)
251            .and_then(|()| state.require_quiescent())
252            .map_err(|error| crate::capture::CaptureError::Invalid(error.to_string()))?;
253        if !state.is_prefill_pending() {
254            return Err(crate::capture::CaptureError::Invalid(
255                "capture and interventions must be configured before generation".into(),
256            ));
257        }
258        Ok(())
259    }
260}
261
262/// Exclusive access for composing native mechanisms at an already settled
263/// continuation boundary. This is a backend/runtime-author surface, not a full
264/// snapshot API. Operations must validate before mutation and preserve or fence
265/// state on failure. Catching an unwind fences this continuation automatically.
266pub struct TextContinuationBoundary<'d, 's, B, C>
267where
268    B: TextGenerationBackend,
269    C: TokenFilterController,
270{
271    runtime: &'d mut ModelRuntime<B>,
272    state: &'s mut TextGenerationContinuation<B, C>,
273}
274
275impl<B: TextGenerationBackend, C: TokenFilterController> TextContinuationBoundary<'_, '_, B, C> {
276    /// Same-run identity used before restoring any state component.
277    pub fn identity(&self) -> TextContinuationIdentity {
278        self.state.identity.clone()
279    }
280
281    /// Exclusive source driver, common to this run and its isolated descendants.
282    pub fn driver_identity(&self) -> TextDriverIdentity {
283        TextDriverIdentity(Arc::clone(&self.state.owner))
284    }
285
286    /// Constraint state for an independently staged host checkpoint.
287    pub fn controller(&self) -> &C {
288        &self.state.inner.controller
289    }
290
291    /// Remaining model-prediction allowance, including any pending input.
292    pub fn remaining_tokens(&self) -> Option<usize> {
293        self.state.inner.remaining_tokens
294    }
295
296    /// Read-only mechanism inputs. No host or native state advances here.
297    #[allow(clippy::type_complexity)]
298    pub fn parts(
299        &self,
300    ) -> (
301        &ModelRuntime<B>,
302        &B::TextGenerationState,
303        Option<PendingTextInput<&B::Prompt, &B::Token>>,
304    ) {
305        (
306            self.runtime,
307            &self.state.inner.backend_state,
308            self.state.inner.step.as_ref().map(PendingTextInput::as_ref),
309        )
310    }
311
312    /// Native mechanism access while the pending input remains read-only.
313    /// Callers retain the existing completion owner and must not submit ordinary
314    /// model predictions through this snapshot-composition borrow.
315    #[allow(clippy::type_complexity)]
316    pub fn mechanism_parts(
317        &mut self,
318    ) -> (
319        &mut ModelRuntime<B>,
320        &mut B::TextGenerationState,
321        Option<PendingTextInput<&B::Prompt, &B::Token>>,
322    ) {
323        (
324            self.runtime,
325            &mut self.state.inner.backend_state,
326            self.state.inner.step.as_ref().map(PendingTextInput::as_ref),
327        )
328    }
329
330    /// Installs independently prepared host state after all fallible validation,
331    /// copying and native installation have succeeded. This performs no work.
332    pub fn install_host_state(
333        &mut self,
334        controller: C,
335        pending: Option<PendingTextInput<B::Prompt, B::Token>>,
336        remaining_tokens: Option<usize>,
337    ) {
338        self.state.inner.controller = controller;
339        self.state.inner.step = pending;
340        self.state.inner.remaining_tokens = remaining_tokens;
341    }
342
343    /// Constructs a child continuation from independently prepared components.
344    /// Its native model state must be installed before advancing it. A child has
345    /// a fresh run identity and shares only this driver's execution authority.
346    pub fn fork_host_state(
347        &self,
348        backend_state: B::TextGenerationState,
349        controller: C,
350        pending: Option<PendingTextInput<B::Prompt, B::Token>>,
351        remaining_tokens: Option<usize>,
352    ) -> TextGenerationContinuation<B, C> {
353        TextGenerationContinuation {
354            owner: Arc::clone(&self.state.owner),
355            identity: TextContinuationIdentity(Arc::new(())),
356            inner: TextGenerationMachine {
357                backend_state,
358                controller,
359                step: pending,
360                completions: Vec::new(),
361                remaining_tokens,
362            },
363            failed: false,
364            records_drained: true,
365        }
366    }
367
368    /// Fences a continuation when a composed operation cannot preserve it.
369    pub fn fail(&mut self) {
370        self.state.failed = true;
371    }
372}
373
374impl<B: TextGenerationBackend, C: TokenFilterController> Drop
375    for TextContinuationBoundary<'_, '_, B, C>
376{
377    fn drop(&mut self) {
378        if std::thread::panicking() {
379            self.state.failed = true;
380        }
381    }
382}
383
384impl<B: crate::execution_control::NativeTextStateBackend> TextGenerationDriver<'_, B> {
385    fn validate_boundary<C: TokenFilterController>(
386        &self,
387        state: &TextGenerationContinuation<B, C>,
388    ) -> Result<(), TextContinuationError<B::Error, C::Error>> {
389        self.validate(state)?;
390        state.require_quiescent()
391    }
392
393    /// Estimates installed or saved native state at a completed continuation
394    /// boundary. Portable composition adds all host-state estimates and reserves
395    /// resources before calling either copying operation below.
396    pub fn estimate_native_state<C: TokenFilterController>(
397        &self,
398        state: &TextGenerationContinuation<B, C>,
399        saved: Option<&B::NativeTextState>,
400    ) -> Result<
401        Option<crate::execution_control::SnapshotEstimate>,
402        TextContinuationError<B::Error, C::Error>,
403    > {
404        self.validate_boundary(state)?;
405        B::estimate_native_text_state(self.runtime, saved)
406            .map_err(|error| ControlledTextGenerationError::Backend(error).into())
407    }
408
409    /// Copies the installed model state through its existing completion owner.
410    /// Caller must first reserve the complete snapshot's resource estimate.
411    pub fn capture_native_state<C: TokenFilterController>(
412        &mut self,
413        state: &TextGenerationContinuation<B, C>,
414    ) -> Result<B::NativeTextState, TextContinuationError<B::Error, C::Error>> {
415        self.validate_boundary(state)?;
416        B::capture_native_text_state(self.runtime)
417            .map_err(|error| ControlledTextGenerationError::Backend(error).into())
418    }
419
420    /// Independently copies a compatible saved slot after resource reservation.
421    /// The currently installed continuation remains unchanged.
422    pub fn copy_native_state<C: TokenFilterController>(
423        &mut self,
424        state: &TextGenerationContinuation<B, C>,
425        saved: &B::NativeTextState,
426    ) -> Result<B::NativeTextState, TextContinuationError<B::Error, C::Error>> {
427        self.validate_boundary(state)?;
428        B::copy_native_text_state(self.runtime, saved)
429            .map_err(|error| ControlledTextGenerationError::Backend(error).into())
430    }
431
432    /// Exchanges the installed native state with a previously copied slot.
433    /// The facade pairs this with the corresponding detached continuation and
434    /// semantic state before advancing again. No token or sampler advances here.
435    pub fn exchange_native_state<C: TokenFilterController>(
436        &mut self,
437        state: &TextGenerationContinuation<B, C>,
438        slot: &mut B::NativeTextState,
439    ) -> Result<(), TextContinuationError<B::Error, C::Error>> {
440        self.validate_boundary(state)?;
441        B::exchange_native_text_state(self.runtime, slot)
442            .map_err(|error| ControlledTextGenerationError::Backend(error).into())
443    }
444}
445
446impl<B: crate::execution_control::NativeTextStateBackend, C: TokenFilterController>
447    TextContinuationBoundary<'_, '_, B, C>
448{
449    /// Exchanges a quiescent child and its model-state slot with the installed
450    /// continuation. All host checks precede native exchange; after its success
451    /// the complete ordinary machine is moved infallibly. The facade then moves
452    /// the matching semantic/lifecycle state before another prediction.
453    pub fn exchange_branch(
454        &mut self,
455        other: &mut TextGenerationContinuation<B, C>,
456        native: &mut B::NativeTextState,
457    ) -> Result<(), TextContinuationError<B::Error, C::Error>> {
458        if !Arc::ptr_eq(&self.state.owner, &other.owner) {
459            return Err(TextContinuationError::IncompatibleDriver);
460        }
461        other.require_quiescent()?;
462        B::validate_native_text_state(self.runtime, native)
463            .map_err(ControlledTextGenerationError::Backend)?;
464        B::exchange_native_text_state(self.runtime, native)
465            .map_err(ControlledTextGenerationError::Backend)?;
466        std::mem::swap(self.state, other);
467        Ok(())
468    }
469}