1use crate::event_bus::EventBus;
9use crate::node_catalog::NodeCatalog;
10use crate::runner::{RunContext, Runner};
11use somatize_compiler::{CompileMode, CompileResult, compile, compile_stream};
12use somatize_core::cache::CacheStore;
13use somatize_core::error::{Result, SomaError};
14use somatize_core::graph::Graph;
15use somatize_core::store::{DataRef, DataStore};
16use somatize_core::value::Value;
17use std::sync::Arc;
18
19pub struct ForwardEnv<'a> {
26 pub catalog: &'a NodeCatalog,
28 pub cache: &'a dyn CacheStore,
30 pub event_bus: &'a Arc<EventBus>,
32 pub data_store: Option<&'a Arc<dyn DataStore>>,
34 pub driver: Option<&'a crate::effects::EffectDriver>,
37}
38
39pub trait ForwardStrategy {
41 fn forward(&self, graph: &Graph, env: &ForwardEnv<'_>, x: &Value) -> Result<Value>;
43}
44
45pub struct Standard;
47
48impl ForwardStrategy for Standard {
49 fn forward(&self, graph: &Graph, env: &ForwardEnv<'_>, x: &Value) -> Result<Value> {
50 let CompileResult { plan, .. } =
51 compile(graph, env.catalog, CompileMode::Inference, Some(env.cache))?;
52 run_forward(graph, &plan, env, x)
53 }
54}
55
56pub struct Stream {
59 pub chunk_size: usize,
61}
62
63impl ForwardStrategy for Stream {
64 fn forward(&self, graph: &Graph, env: &ForwardEnv<'_>, x: &Value) -> Result<Value> {
65 let CompileResult { plan, .. } = compile_stream(graph, env.catalog, self.chunk_size)?;
66 run_forward(graph, &plan, env, x)
67 }
68}
69
70fn run_forward(
78 graph: &Graph,
79 plan: &somatize_compiler::ExecutionPlan,
80 env: &ForwardEnv<'_>,
81 x: &Value,
82) -> Result<Value> {
83 let run_id = somatize_core::util::timestamp_id("forward");
84 let mut ctx = RunContext::new(
85 env.catalog,
86 env.cache,
87 env.event_bus,
88 &run_id,
89 crate::executor::GraphInfo::from_graph(graph),
90 );
91 if let Some(driver) = env.driver {
92 ctx = ctx.with_driver(driver.clone());
93 }
94 crate::runner::LocalRunner.forward(plan, &ctx, x)
95}
96
97pub struct Batched<'a> {
100 pub data_ref: &'a DataRef,
102 pub batch_size: usize,
104}
105
106impl ForwardStrategy for Batched<'_> {
107 fn forward(&self, graph: &Graph, env: &ForwardEnv<'_>, _x: &Value) -> Result<Value> {
108 let store = env.data_store.ok_or_else(|| SomaError::Execution {
109 node_id: "session".into(),
110 message: "Batched strategy requires a data store (use with_data_store)".into(),
111 })?;
112
113 let meta = store.meta(self.data_ref)?;
114 let total_rows = meta.total_rows;
115 if total_rows == 0 {
116 return Ok(Value::Empty);
117 }
118
119 let CompileResult { plan, .. } =
121 compile(graph, env.catalog, CompileMode::Inference, Some(env.cache))?;
122
123 let mut all_values: Vec<f64> = Vec::new();
124 let mut result_shape: Option<Vec<usize>> = None;
125 let mut rows_processed = 0;
126
127 while rows_processed < total_rows {
128 let batch_len = self.batch_size.min(total_rows - rows_processed);
129 let batch = store.get_rows(self.data_ref, rows_processed, batch_len)?;
130 let output = run_forward(graph, &plan, env, &batch)?;
131
132 if let Value::Tensor { values, shape } = &output {
133 if result_shape.is_none() {
134 result_shape = Some(shape.clone());
135 }
136 all_values.extend_from_slice(values.as_slice());
137 } else {
138 return Ok(output);
139 }
140
141 rows_processed += batch_len;
142 }
143
144 match result_shape {
145 Some(mut shape) => {
146 shape[0] = total_rows;
147 Ok(Value::tensor(all_values, shape))
148 }
149 None => Ok(Value::Empty),
150 }
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::cache::MemoryCache;
158 use crate::node_catalog::NodeCatalog;
159 use somatize_core::cache::CacheKey;
160 use somatize_core::error::Result as SomaResult;
161 use somatize_core::filter::{Distribution, Filter, FilterKind, FilterMeta, StreamMode};
162 use somatize_core::graph::{Graph, Node};
163
164 struct DoublerFilter;
165 impl Filter for DoublerFilter {
166 fn config_hash(&self) -> CacheKey {
167 CacheKey::from_parts(&[b"Doubler"])
168 }
169 fn fit(&self, _x: &Value, _y: Option<&Value>) -> SomaResult<Value> {
170 Ok(Value::Empty)
171 }
172 fn forward(&self, x: &Value, _state: &Value) -> SomaResult<Value> {
173 match x {
174 Value::Tensor { values, shape } => {
175 let doubled: Vec<f64> = values.iter().map(|v| v * 2.0).collect();
176 Ok(Value::tensor(doubled, shape.clone()))
177 }
178 _ => Ok(x.clone()),
179 }
180 }
181 fn meta(&self) -> FilterMeta {
182 FilterMeta {
183 name: "Doubler".into(),
184 kind: FilterKind::Stateless,
185 cacheable: false,
186 differentiable: false,
187 deterministic: true,
188 stream_mode: StreamMode::FixedState,
189 distribution: Distribution::Local,
190 input_schema: None,
191 output_schema: None,
192 }
193 }
194 }
195
196 fn make_session() -> (Graph, NodeCatalog, Arc<dyn CacheStore>, Arc<EventBus>) {
197 let mut graph = Graph::new();
198 graph.nodes.push(Node::new("double", "Double", "double"));
199
200 let mut catalog = NodeCatalog::new();
201 catalog.register("double", Box::new(DoublerFilter));
202
203 let cache: Arc<dyn CacheStore> = Arc::new(MemoryCache::default());
204 let bus = Arc::new(EventBus::new(64));
205 (graph, catalog, cache, bus)
206 }
207
208 fn env<'a>(
209 catalog: &'a NodeCatalog,
210 cache: &'a dyn CacheStore,
211 event_bus: &'a Arc<EventBus>,
212 ) -> ForwardEnv<'a> {
213 ForwardEnv {
214 catalog,
215 cache,
216 event_bus,
217 data_store: None,
218 driver: None,
219 }
220 }
221
222 #[test]
223 fn standard_forward() {
224 let (graph, catalog, cache, bus) = make_session();
225 let input = Value::tensor(vec![1.0, 2.0, 3.0], vec![3]);
226
227 let result = Standard
228 .forward(&graph, &env(&catalog, cache.as_ref(), &bus), &input)
229 .unwrap();
230 let (data, _) = result.as_tensor().unwrap();
231 assert_eq!(data, &[2.0, 4.0, 6.0]);
232 }
233
234 #[test]
235 fn stream_forward() {
236 let (graph, catalog, cache, bus) = make_session();
237 let input = Value::tensor(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], vec![6]);
238
239 let result = Stream { chunk_size: 2 }
240 .forward(&graph, &env(&catalog, cache.as_ref(), &bus), &input)
241 .unwrap();
242 let (data, shape) = result.as_tensor().unwrap();
243 assert_eq!(data, &[2.0, 4.0, 6.0, 8.0, 10.0, 12.0]);
244 assert_eq!(shape, &[6]);
245 }
246
247 #[test]
248 fn stream_matches_standard() {
249 let (graph, catalog, cache, bus) = make_session();
250 let input = Value::tensor(vec![1.0, 2.0, 3.0, 4.0], vec![4]);
251
252 let standard = Standard
253 .forward(&graph, &env(&catalog, cache.as_ref(), &bus), &input)
254 .unwrap();
255 let streamed = Stream { chunk_size: 2 }
256 .forward(&graph, &env(&catalog, cache.as_ref(), &bus), &input)
257 .unwrap();
258 assert_eq!(standard, streamed);
259 }
260}