Skip to main content

sim_lib_compute_model/
ode.rs

1//! Resident tensor ODE execution over the canonical numeric/RK pipeline.
2
3use std::sync::Arc;
4
5use sim_kernel::{
6    Consistency, Cx, Error, EvalFabric, EvalMode, EvalReply, EvalRequest, Expr, Result, Symbol,
7};
8use sim_lib_numbers_tensor::{
9    SubmissionEvidence, TensorExecutor, TensorExecutorCard, TensorSite, tensor_value_ref,
10};
11
12use crate::model::{ModeledComputeProfile, ModeledComputeSnapshot, ModeledTensorExecutor};
13use crate::site::compute_model_site_symbol;
14
15/// ODE execution family requested by the resident adapter.
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum ResidentOdeKind {
18    /// Fixed-step RK methods.
19    Fixed,
20    /// Adaptive RK methods with scalar error decisions.
21    Adaptive,
22}
23
24/// Provider-side lowering classification for a tensor RHS.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum ResidentRhsLowering {
27    /// RHS is a lowerable tensor expression.
28    TensorExpression,
29    /// RHS is host-only or effectful and must not be accepted by resident ODE.
30    NonLowerable,
31}
32
33/// Checked resident ODE plan.
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct ResidentOdePlan {
36    kind: ResidentOdeKind,
37    state_shape: Vec<usize>,
38    dtype: Symbol,
39}
40
41impl ResidentOdePlan {
42    /// Builds a fixed-step resident ODE plan for tensor state.
43    pub fn fixed(state_shape: Vec<usize>, dtype: Symbol, rhs: ResidentRhsLowering) -> Result<Self> {
44        Self::new(ResidentOdeKind::Fixed, state_shape, dtype, rhs)
45    }
46
47    /// Builds an adaptive resident ODE plan for tensor state.
48    pub fn adaptive(
49        state_shape: Vec<usize>,
50        dtype: Symbol,
51        rhs: ResidentRhsLowering,
52    ) -> Result<Self> {
53        Self::new(ResidentOdeKind::Adaptive, state_shape, dtype, rhs)
54    }
55
56    fn new(
57        kind: ResidentOdeKind,
58        state_shape: Vec<usize>,
59        dtype: Symbol,
60        rhs: ResidentRhsLowering,
61    ) -> Result<Self> {
62        if state_shape.iter().product::<usize>() < 2 {
63            return Err(Error::Eval(
64                "resident ODE declines scalar or too-small tensor state".to_owned(),
65            ));
66        }
67        if rhs != ResidentRhsLowering::TensorExpression {
68            return Err(Error::Eval(
69                "resident ODE requires a lowerable tensor RHS expression".to_owned(),
70            ));
71        }
72        Ok(Self {
73            kind,
74            state_shape,
75            dtype,
76        })
77    }
78
79    /// Returns the execution family.
80    pub fn kind(&self) -> ResidentOdeKind {
81        self.kind
82    }
83
84    /// Returns the planned tensor state shape.
85    pub fn state_shape(&self) -> &[usize] {
86        &self.state_shape
87    }
88
89    /// Returns the planned tensor dtype.
90    pub fn dtype(&self) -> &Symbol {
91        &self.dtype
92    }
93}
94
95/// Result plus provider counters for a resident ODE execution.
96#[derive(Clone)]
97pub struct ResidentOdeExecution {
98    /// Eval reply produced by the canonical numeric pipeline.
99    pub reply: EvalReply,
100    /// Executor descriptor used for the resident tensor placement.
101    pub executor: TensorExecutorCard,
102    /// Counter snapshot after final synchronization when the executor is modeled.
103    pub modeled_snapshot: Option<ModeledComputeSnapshot>,
104    /// Resident readbacks performed while executing the request when known.
105    pub modeled_readbacks: Option<usize>,
106    /// Accepted tensor submissions represented by the final flush.
107    pub final_flush: SubmissionEvidence,
108}
109
110/// Executes checked tensor ODE plans through an active tensor executor.
111#[derive(Clone)]
112pub struct ResidentOdeExecutor {
113    site: Symbol,
114    executor: Arc<dyn TensorExecutor>,
115    modeled: Option<ModeledTensorExecutor>,
116}
117
118impl ResidentOdeExecutor {
119    /// Builds a resident ODE executor over an already-selected tensor executor.
120    pub fn with_executor(site: Symbol, executor: Arc<dyn TensorExecutor>) -> Self {
121        Self {
122            site,
123            executor,
124            modeled: None,
125        }
126    }
127
128    /// Builds an ODE executor over the modeled resident provider.
129    pub fn modeled(mut profile: ModeledComputeProfile) -> Self {
130        profile.auto_flush_batches = true;
131        let executor = ModeledTensorExecutor::new(profile);
132        Self {
133            site: compute_model_site_symbol(),
134            executor: Arc::new(executor.clone()) as Arc<dyn TensorExecutor>,
135            modeled: Some(executor),
136        }
137    }
138
139    /// Returns the underlying modeled tensor executor.
140    pub fn modeled_tensor_executor(&self) -> Option<&ModeledTensorExecutor> {
141        self.modeled.as_ref()
142    }
143
144    /// Executes an ODE expression through the resident tensor site.
145    pub fn execute(
146        &self,
147        cx: &mut Cx,
148        plan: &ResidentOdePlan,
149        expr: Expr,
150    ) -> Result<ResidentOdeExecution> {
151        let before = self.modeled.as_ref().map(ModeledTensorExecutor::snapshot);
152        let site = TensorSite::new(self.site.clone(), self.executor.clone(), Vec::new());
153        let request = eval_request(expr);
154        let reply = if plan.kind() == ResidentOdeKind::Fixed {
155            if let Some(executor) = &self.modeled {
156                executor.begin_internal_materialization();
157            }
158            let reply = site.realize(cx, request);
159            if let Some(executor) = &self.modeled {
160                executor.end_internal_materialization();
161            }
162            reply?
163        } else {
164            site.realize(cx, request)?
165        };
166        let value = if let Some(table) = reply.value.object().as_table_impl() {
167            table.get(cx, Symbol::new("value"))?
168        } else {
169            reply.value.clone()
170        };
171        let tensor = tensor_value_ref(&value).ok_or_else(|| {
172            Error::Eval("resident ODE result did not produce tensor state".to_owned())
173        })?;
174        if tensor.shape() != plan.state_shape() || tensor.dtype() != plan.dtype() {
175            return Err(Error::Eval(format!(
176                "resident ODE result shape/dtype {:?}/{} did not match plan {:?}/{}",
177                tensor.shape(),
178                tensor.dtype(),
179                plan.state_shape(),
180                plan.dtype()
181            )));
182        }
183        let final_flush = self.executor.flush().map_err(Error::from)?;
184        let snapshot = self.modeled.as_ref().map(ModeledTensorExecutor::snapshot);
185        let readbacks = match (&before, &snapshot) {
186            (Some(before), Some(snapshot)) => {
187                Some(snapshot.readbacks.saturating_sub(before.readbacks))
188            }
189            _ => None,
190        };
191        Ok(ResidentOdeExecution {
192            reply,
193            executor: self.executor.card(),
194            modeled_snapshot: snapshot,
195            modeled_readbacks: readbacks,
196            final_flush,
197        })
198    }
199}
200
201impl ResidentOdeExecutor {
202    /// Builds an ODE executor that auto-flushes bounded modeled tensor batches.
203    pub fn new(profile: ModeledComputeProfile) -> Self {
204        Self::modeled(profile)
205    }
206}
207
208impl Default for ResidentOdeExecutor {
209    fn default() -> Self {
210        Self::modeled(ModeledComputeProfile::default())
211    }
212}
213
214fn eval_request(expr: Expr) -> EvalRequest {
215    EvalRequest {
216        expr,
217        result_shape: None,
218        required_capabilities: Vec::new(),
219        deadline: None,
220        consistency: Consistency::LocalFirst,
221        mode: EvalMode::Eval,
222        answer_limit: None,
223        stream_buffer: None,
224        stream: false,
225        trace: false,
226    }
227}