1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum ResidentOdeKind {
18 Fixed,
20 Adaptive,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum ResidentRhsLowering {
27 TensorExpression,
29 NonLowerable,
31}
32
33#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct ResidentOdePlan {
36 kind: ResidentOdeKind,
37 state_shape: Vec<usize>,
38 dtype: Symbol,
39}
40
41impl ResidentOdePlan {
42 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 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 pub fn kind(&self) -> ResidentOdeKind {
81 self.kind
82 }
83
84 pub fn state_shape(&self) -> &[usize] {
86 &self.state_shape
87 }
88
89 pub fn dtype(&self) -> &Symbol {
91 &self.dtype
92 }
93}
94
95#[derive(Clone)]
97pub struct ResidentOdeExecution {
98 pub reply: EvalReply,
100 pub executor: TensorExecutorCard,
102 pub modeled_snapshot: Option<ModeledComputeSnapshot>,
104 pub modeled_readbacks: Option<usize>,
106 pub final_flush: SubmissionEvidence,
108}
109
110#[derive(Clone)]
112pub struct ResidentOdeExecutor {
113 site: Symbol,
114 executor: Arc<dyn TensorExecutor>,
115 modeled: Option<ModeledTensorExecutor>,
116}
117
118impl ResidentOdeExecutor {
119 pub fn with_executor(site: Symbol, executor: Arc<dyn TensorExecutor>) -> Self {
121 Self {
122 site,
123 executor,
124 modeled: None,
125 }
126 }
127
128 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 pub fn modeled_tensor_executor(&self) -> Option<&ModeledTensorExecutor> {
141 self.modeled.as_ref()
142 }
143
144 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 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}