1use crate::error::{Error, Result};
15use crate::imf::loader::ArtifactGraphs;
16use crate::imf::tokens;
17
18fn model_err(what: impl Into<String>) -> Error {
19 Error::Model(what.into())
20}
21
22pub trait InferenceEngine {
26 fn encode(&mut self, input_ids: &[i64]) -> Result<Vec<f32>>;
29 fn hidden_dims(&self, seq_len: usize) -> Vec<i64>;
32 fn decode_step(&mut self, decoder_ids: &[i64], hidden: &[f32]) -> Result<Vec<f32>>;
35}
36
37pub fn greedy_generate(
41 engine: &mut dyn InferenceEngine,
42 text: &str,
43 max_len: usize,
44) -> Result<Vec<i64>> {
45 let ids = tokens::encode(text);
46 if ids.len() == 1 {
47 return Ok(Vec::new());
49 }
50 let hidden = engine.encode(&ids)?;
51 let mut decoder_ids = vec![tokens::PAD_ID];
52 let mut generated = Vec::new();
53 for _ in 0..max_len {
54 let logits = engine.decode_step(&decoder_ids, &hidden)?;
55 let token = argmax(&logits).ok_or_else(|| model_err("decoder produced empty logits"))?;
57 if token == tokens::EOS_ID {
58 break;
59 }
60 generated.push(token);
61 decoder_ids.push(token);
62 }
63 Ok(generated)
64}
65
66pub fn translate(engine: &mut dyn InferenceEngine, text: &str, max_len: usize) -> Result<String> {
71 let ids = greedy_generate(engine, text, max_len)?;
72 Ok(tokens::decode(&ids))
73}
74
75pub fn ids_to_fixture_bytes(ids: &[i64]) -> Vec<u8> {
80 ids.iter()
81 .map(|&t| std::cmp::max(0, t - tokens::BYTE_OFFSET) as u8)
82 .collect()
83}
84
85pub fn ids_to_fixture_string(ids: &[i64]) -> String {
86 String::from_utf8_lossy(&ids_to_fixture_bytes(ids)).into_owned()
87}
88
89fn argmax(data: &[f32]) -> Option<i64> {
90 let mut best: Option<(usize, f32)> = None;
91 for (i, &v) in data.iter().enumerate() {
92 match best {
93 Some((_, bv)) if bv >= v => {}
94 _ => best = Some((i, v)),
95 }
96 }
97 best.map(|(i, _)| i as i64)
98}
99
100pub const MAX_DECODE_STEPS: usize = 512;
103
104pub struct OrtEngine {
111 encoder: ort::session::Session,
112 decoder: ort::session::Session,
113 encoder_input: String,
114 decoder_inputs: [String; 2],
115 hidden_seq: usize,
116 hidden_dim: usize,
117}
118
119fn ort_environment() -> Result<&'static ort::environment::Environment> {
124 static ENV: std::sync::OnceLock<std::result::Result<ort::environment::Environment, String>> =
125 std::sync::OnceLock::new();
126 ENV.get_or_init(|| {
127 ort::init()
128 .with_name("iscrs")
129 .build()
130 .map_err(|e| e.to_string())
131 })
132 .as_ref()
133 .map_err(|e| model_err(format!("onnxruntime environment init failed: {e}")))
134}
135
136impl OrtEngine {
137 pub fn from_graphs(graphs: &ArtifactGraphs) -> Result<OrtEngine> {
142 let intra_threads = std::thread::available_parallelism()
143 .map(|n| n.get())
144 .unwrap_or(1);
145 let intra_threads = std::cmp::max(1, intra_threads / 2);
146
147 let env = ort_environment()?;
148
149 let commit = |bytes: &[u8]| -> Result<ort::session::Session> {
150 let builder = ort::session::Session::builder(env)
151 .map_err(|e| model_err(format!("session builder failed: {e}")))?;
152 let builder = builder
153 .with_optimization_level(
154 ort::session::builder::GraphOptimizationLevel::Level3,
155 )
156 .map_err(|e| model_err(format!("optimization level failed: {e}")))?;
157 let builder = builder
158 .with_intra_threads(intra_threads)
159 .map_err(|e| model_err(format!("intra threads failed: {e}")))?;
160 let mut builder = builder;
161 builder
162 .commit_from_memory(bytes)
163 .map_err(|e| model_err(format!("onnx session load failed: {e}")))
164 };
165
166 let encoder = commit(&graphs.graphs["encoder.onnx"])?;
167 let decoder_name = graphs.decoder_member()?;
168 let decoder = commit(&graphs
169 .graphs
170 .get(decoder_name)
171 .ok_or_else(|| model_err(format!("missing verified member {decoder_name}")))?)?;
172
173 let encoder_input = encoder
174 .inputs()
175 .first()
176 .map(|i| i.name().to_string())
177 .ok_or_else(|| model_err("encoder graph has no inputs"))?;
178 let dec_inputs: Vec<String> = decoder
179 .inputs()
180 .iter()
181 .take(2)
182 .map(|i| i.name().to_string())
183 .collect();
184 if dec_inputs.len() != 2 {
185 return Err(model_err(
186 "decoder graph lacks input_ids/encoder_hidden_states inputs",
187 ));
188 }
189 let decoder_inputs = [dec_inputs[0].clone(), dec_inputs[1].clone()];
190
191 Ok(OrtEngine {
192 encoder,
193 decoder,
194 encoder_input,
195 decoder_inputs,
196 hidden_seq: 0,
197 hidden_dim: 0,
198 })
199 }
200}
201
202impl InferenceEngine for OrtEngine {
203 fn encode(&mut self, input_ids: &[i64]) -> Result<Vec<f32>> {
204 let seq = input_ids.len();
205 let tensor = ort::value::Tensor::from_array(([1usize, seq], input_ids.to_vec()))
206 .map_err(|e| model_err(format!("input_ids tensor build failed: {e}")))?;
207 let key = self
208 .encoder
209 .outputs()
210 .first()
211 .map(|o| o.name().to_string())
212 .ok_or_else(|| model_err("encoder graph has no outputs"))?;
213 let outputs = self
214 .encoder
215 .run(ort::inputs![self.encoder_input.as_str() => tensor])
216 .map_err(|e| model_err(format!("encoder run failed: {e}")))?;
217 let (shape, data) = outputs
218 .get(key.as_str())
219 .ok_or_else(|| model_err(format!("encoder output {key:?} missing")))?
220 .try_extract_tensor::<f32>()
221 .map_err(|e| model_err(format!("encoder output extract failed: {e}")))?;
222 let dims: Vec<i64> = shape.iter().copied().collect();
224 self.hidden_seq = dims.get(1).copied().unwrap_or(seq as i64) as usize;
225 self.hidden_dim = dims
226 .get(2)
227 .copied()
228 .unwrap_or((data.len() / seq.max(1)) as i64) as usize;
229 Ok(data.to_vec())
230 }
231
232 fn hidden_dims(&self, seq_len: usize) -> Vec<i64> {
233 let seq = if self.hidden_seq == 0 { seq_len } else { self.hidden_seq };
234 let d = if self.hidden_dim == 0 { 1 } else { self.hidden_dim };
235 vec![1, seq as i64, d as i64]
236 }
237
238 fn decode_step(&mut self, decoder_ids: &[i64], hidden: &[f32]) -> Result<Vec<f32>> {
239 let cur_len = decoder_ids.len();
240 let ids = ort::value::Tensor::from_array(([1usize, cur_len], decoder_ids.to_vec()))
241 .map_err(|e| model_err(format!("decoder input_ids tensor build failed: {e}")))?;
242 let dims = self.hidden_dims(hidden.len());
243 let states = ort::value::Tensor::from_array((dims, hidden.to_vec()))
244 .map_err(|e| model_err(format!("encoder_hidden_states tensor build failed: {e}")))?;
245 let key = self
246 .decoder
247 .outputs()
248 .first()
249 .map(|o| o.name().to_string())
250 .ok_or_else(|| model_err("decoder graph has no outputs"))?;
251 let outputs = self
252 .decoder
253 .run(ort::inputs![
254 self.decoder_inputs[0].as_str() => ids,
255 self.decoder_inputs[1].as_str() => states,
256 ])
257 .map_err(|e| model_err(format!("decoder run failed: {e}")))?;
258 let (shape, data) = outputs
259 .get(key.as_str())
260 .ok_or_else(|| model_err(format!("decoder output {key:?} missing")))?
261 .try_extract_tensor::<f32>()
262 .map_err(|e| model_err(format!("decoder output extract failed: {e}")))?;
263 let dims: Vec<i64> = shape.iter().copied().collect();
266 let vocab = dims
267 .get(2)
268 .copied()
269 .unwrap_or((data.len() / cur_len.max(1)) as i64) as usize;
270 let start = (cur_len - 1) * vocab;
271 let end = (start + vocab).min(data.len());
272 if start >= data.len() {
273 return Err(model_err("decoder logits too short for prefix length"));
274 }
275 Ok(data[start..end].to_vec())
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 struct MockEngine {
286 steps: std::cell::RefCell<Vec<Vec<i64>>>,
287 }
288
289 impl MockEngine {
290 fn new() -> Self {
291 MockEngine {
292 steps: std::cell::RefCell::new(Vec::new()),
293 }
294 }
295 }
296
297 impl InferenceEngine for MockEngine {
298 fn encode(&mut self, input_ids: &[i64]) -> Result<Vec<f32>> {
299 assert_eq!(input_ids.last(), Some(&tokens::EOS_ID));
300 assert_eq!(input_ids[0], b'x' as i64 + tokens::BYTE_OFFSET);
301 Ok(vec![1.0])
302 }
303 fn hidden_dims(&self, _seq: usize) -> Vec<i64> {
304 vec![1, 1, 1]
305 }
306 fn decode_step(&mut self, decoder_ids: &[i64], _hidden: &[f32]) -> Result<Vec<f32>> {
307 self.steps.borrow_mut().push(decoder_ids.to_vec());
308 let vocab = 300i64;
309 let mut logits = vec![0.0f32; vocab as usize];
310 match decoder_ids.len() {
311 1 => logits['a' as i64 as usize] = 5.0, 2 => logits['a' as i64 as usize] = 4.0, _ => logits[tokens::EOS_ID as usize] = 3.0, }
315 Ok(logits)
316 }
317 }
318
319 #[test]
320 fn greedy_loop_stops_at_eos_and_grows_prefix() {
321 let mut engine = MockEngine::new();
322 let ids = greedy_generate(&mut engine, "x", MAX_DECODE_STEPS).unwrap();
323 assert_eq!(ids, vec!['a' as i64, 'a' as i64]);
324 assert_eq!(engine.steps.borrow()[0], vec![tokens::PAD_ID]);
326 assert_eq!(
327 engine.steps.borrow()[1],
328 vec![tokens::PAD_ID, 'a' as i64]
329 );
330 }
331
332 #[test]
333 fn empty_input_short_circuits() {
334 let mut engine = MockEngine::new();
335 assert!(greedy_generate(&mut engine, "", MAX_DECODE_STEPS)
336 .unwrap()
337 .is_empty());
338 assert!(engine.steps.borrow().is_empty());
339 }
340
341 #[test]
342 fn max_len_cap_bounds_the_loop() {
343 struct NeverEos;
344 impl InferenceEngine for NeverEos {
345 fn encode(&mut self, _ids: &[i64]) -> Result<Vec<f32>> {
346 Ok(vec![1.0])
347 }
348 fn hidden_dims(&self, _s: usize) -> Vec<i64> {
349 vec![1, 1, 1]
350 }
351 fn decode_step(&mut self, _ids: &[i64], _h: &[f32]) -> Result<Vec<f32>> {
352 let mut logits = vec![0.0f32; 300];
353 logits[100] = 1.0;
354 Ok(logits)
355 }
356 }
357 let mut engine = NeverEos;
358 let ids = greedy_generate(&mut engine, "x", 10).unwrap();
359 assert_eq!(ids.len(), 10);
360 assert!(ids.iter().all(|&t| t == 100));
361 }
362
363 #[test]
364 fn fixture_byte_rule_and_argmax_tiebreak() {
365 assert_eq!(ids_to_fixture_bytes(&[97, 2, 0, 101]), b"^\x00\x00b");
368 assert_eq!(crate::imf::tokens::decode(&[97, 2, 0, 101]), "^b");
370 assert_eq!(argmax(&[1.0, 3.0, 3.0]), Some(1)); assert_eq!(argmax(&[]), None);
372 }
373}