use crate::error::{Error, Result};
use crate::imf::loader::ArtifactGraphs;
use crate::imf::tokens;
fn model_err(what: impl Into<String>) -> Error {
Error::Model(what.into())
}
pub trait InferenceEngine {
fn encode(&mut self, input_ids: &[i64]) -> Result<Vec<f32>>;
fn hidden_dims(&self, seq_len: usize) -> Vec<i64>;
fn decode_step(&mut self, decoder_ids: &[i64], hidden: &[f32]) -> Result<Vec<f32>>;
}
pub fn greedy_generate(
engine: &mut dyn InferenceEngine,
text: &str,
max_len: usize,
) -> Result<Vec<i64>> {
let ids = tokens::encode(text);
if ids.len() == 1 {
return Ok(Vec::new());
}
let hidden = engine.encode(&ids)?;
let mut decoder_ids = vec![tokens::PAD_ID];
let mut generated = Vec::new();
for _ in 0..max_len {
let logits = engine.decode_step(&decoder_ids, &hidden)?;
let token = argmax(&logits).ok_or_else(|| model_err("decoder produced empty logits"))?;
if token == tokens::EOS_ID {
break;
}
generated.push(token);
decoder_ids.push(token);
}
Ok(generated)
}
pub fn translate(engine: &mut dyn InferenceEngine, text: &str, max_len: usize) -> Result<String> {
let ids = greedy_generate(engine, text, max_len)?;
Ok(tokens::decode(&ids))
}
pub fn ids_to_fixture_bytes(ids: &[i64]) -> Vec<u8> {
ids.iter()
.map(|&t| std::cmp::max(0, t - tokens::BYTE_OFFSET) as u8)
.collect()
}
pub fn ids_to_fixture_string(ids: &[i64]) -> String {
String::from_utf8_lossy(&ids_to_fixture_bytes(ids)).into_owned()
}
fn argmax(data: &[f32]) -> Option<i64> {
let mut best: Option<(usize, f32)> = None;
for (i, &v) in data.iter().enumerate() {
match best {
Some((_, bv)) if bv >= v => {}
_ => best = Some((i, v)),
}
}
best.map(|(i, _)| i as i64)
}
pub const MAX_DECODE_STEPS: usize = 512;
pub struct OrtEngine {
encoder: ort::session::Session,
decoder: ort::session::Session,
encoder_input: String,
decoder_inputs: [String; 2],
hidden_seq: usize,
hidden_dim: usize,
}
fn ort_environment() -> Result<&'static ort::environment::Environment> {
static ENV: std::sync::OnceLock<std::result::Result<ort::environment::Environment, String>> =
std::sync::OnceLock::new();
ENV.get_or_init(|| {
ort::init()
.with_name("iscrs")
.build()
.map_err(|e| e.to_string())
})
.as_ref()
.map_err(|e| model_err(format!("onnxruntime environment init failed: {e}")))
}
impl OrtEngine {
pub fn from_graphs(graphs: &ArtifactGraphs) -> Result<OrtEngine> {
let intra_threads = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
let intra_threads = std::cmp::max(1, intra_threads / 2);
let env = ort_environment()?;
let commit = |bytes: &[u8]| -> Result<ort::session::Session> {
let builder = ort::session::Session::builder(env)
.map_err(|e| model_err(format!("session builder failed: {e}")))?;
let builder = builder
.with_optimization_level(
ort::session::builder::GraphOptimizationLevel::Level3,
)
.map_err(|e| model_err(format!("optimization level failed: {e}")))?;
let builder = builder
.with_intra_threads(intra_threads)
.map_err(|e| model_err(format!("intra threads failed: {e}")))?;
let mut builder = builder;
builder
.commit_from_memory(bytes)
.map_err(|e| model_err(format!("onnx session load failed: {e}")))
};
let encoder = commit(&graphs.graphs["encoder.onnx"])?;
let decoder_name = graphs.decoder_member()?;
let decoder = commit(&graphs
.graphs
.get(decoder_name)
.ok_or_else(|| model_err(format!("missing verified member {decoder_name}")))?)?;
let encoder_input = encoder
.inputs()
.first()
.map(|i| i.name().to_string())
.ok_or_else(|| model_err("encoder graph has no inputs"))?;
let dec_inputs: Vec<String> = decoder
.inputs()
.iter()
.take(2)
.map(|i| i.name().to_string())
.collect();
if dec_inputs.len() != 2 {
return Err(model_err(
"decoder graph lacks input_ids/encoder_hidden_states inputs",
));
}
let decoder_inputs = [dec_inputs[0].clone(), dec_inputs[1].clone()];
Ok(OrtEngine {
encoder,
decoder,
encoder_input,
decoder_inputs,
hidden_seq: 0,
hidden_dim: 0,
})
}
}
impl InferenceEngine for OrtEngine {
fn encode(&mut self, input_ids: &[i64]) -> Result<Vec<f32>> {
let seq = input_ids.len();
let tensor = ort::value::Tensor::from_array(([1usize, seq], input_ids.to_vec()))
.map_err(|e| model_err(format!("input_ids tensor build failed: {e}")))?;
let key = self
.encoder
.outputs()
.first()
.map(|o| o.name().to_string())
.ok_or_else(|| model_err("encoder graph has no outputs"))?;
let outputs = self
.encoder
.run(ort::inputs![self.encoder_input.as_str() => tensor])
.map_err(|e| model_err(format!("encoder run failed: {e}")))?;
let (shape, data) = outputs
.get(key.as_str())
.ok_or_else(|| model_err(format!("encoder output {key:?} missing")))?
.try_extract_tensor::<f32>()
.map_err(|e| model_err(format!("encoder output extract failed: {e}")))?;
let dims: Vec<i64> = shape.iter().copied().collect();
self.hidden_seq = dims.get(1).copied().unwrap_or(seq as i64) as usize;
self.hidden_dim = dims
.get(2)
.copied()
.unwrap_or((data.len() / seq.max(1)) as i64) as usize;
Ok(data.to_vec())
}
fn hidden_dims(&self, seq_len: usize) -> Vec<i64> {
let seq = if self.hidden_seq == 0 { seq_len } else { self.hidden_seq };
let d = if self.hidden_dim == 0 { 1 } else { self.hidden_dim };
vec![1, seq as i64, d as i64]
}
fn decode_step(&mut self, decoder_ids: &[i64], hidden: &[f32]) -> Result<Vec<f32>> {
let cur_len = decoder_ids.len();
let ids = ort::value::Tensor::from_array(([1usize, cur_len], decoder_ids.to_vec()))
.map_err(|e| model_err(format!("decoder input_ids tensor build failed: {e}")))?;
let dims = self.hidden_dims(hidden.len());
let states = ort::value::Tensor::from_array((dims, hidden.to_vec()))
.map_err(|e| model_err(format!("encoder_hidden_states tensor build failed: {e}")))?;
let key = self
.decoder
.outputs()
.first()
.map(|o| o.name().to_string())
.ok_or_else(|| model_err("decoder graph has no outputs"))?;
let outputs = self
.decoder
.run(ort::inputs![
self.decoder_inputs[0].as_str() => ids,
self.decoder_inputs[1].as_str() => states,
])
.map_err(|e| model_err(format!("decoder run failed: {e}")))?;
let (shape, data) = outputs
.get(key.as_str())
.ok_or_else(|| model_err(format!("decoder output {key:?} missing")))?
.try_extract_tensor::<f32>()
.map_err(|e| model_err(format!("decoder output extract failed: {e}")))?;
let dims: Vec<i64> = shape.iter().copied().collect();
let vocab = dims
.get(2)
.copied()
.unwrap_or((data.len() / cur_len.max(1)) as i64) as usize;
let start = (cur_len - 1) * vocab;
let end = (start + vocab).min(data.len());
if start >= data.len() {
return Err(model_err("decoder logits too short for prefix length"));
}
Ok(data[start..end].to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockEngine {
steps: std::cell::RefCell<Vec<Vec<i64>>>,
}
impl MockEngine {
fn new() -> Self {
MockEngine {
steps: std::cell::RefCell::new(Vec::new()),
}
}
}
impl InferenceEngine for MockEngine {
fn encode(&mut self, input_ids: &[i64]) -> Result<Vec<f32>> {
assert_eq!(input_ids.last(), Some(&tokens::EOS_ID));
assert_eq!(input_ids[0], b'x' as i64 + tokens::BYTE_OFFSET);
Ok(vec![1.0])
}
fn hidden_dims(&self, _seq: usize) -> Vec<i64> {
vec![1, 1, 1]
}
fn decode_step(&mut self, decoder_ids: &[i64], _hidden: &[f32]) -> Result<Vec<f32>> {
self.steps.borrow_mut().push(decoder_ids.to_vec());
let vocab = 300i64;
let mut logits = vec![0.0f32; vocab as usize];
match decoder_ids.len() {
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, }
Ok(logits)
}
}
#[test]
fn greedy_loop_stops_at_eos_and_grows_prefix() {
let mut engine = MockEngine::new();
let ids = greedy_generate(&mut engine, "x", MAX_DECODE_STEPS).unwrap();
assert_eq!(ids, vec!['a' as i64, 'a' as i64]);
assert_eq!(engine.steps.borrow()[0], vec![tokens::PAD_ID]);
assert_eq!(
engine.steps.borrow()[1],
vec![tokens::PAD_ID, 'a' as i64]
);
}
#[test]
fn empty_input_short_circuits() {
let mut engine = MockEngine::new();
assert!(greedy_generate(&mut engine, "", MAX_DECODE_STEPS)
.unwrap()
.is_empty());
assert!(engine.steps.borrow().is_empty());
}
#[test]
fn max_len_cap_bounds_the_loop() {
struct NeverEos;
impl InferenceEngine for NeverEos {
fn encode(&mut self, _ids: &[i64]) -> Result<Vec<f32>> {
Ok(vec![1.0])
}
fn hidden_dims(&self, _s: usize) -> Vec<i64> {
vec![1, 1, 1]
}
fn decode_step(&mut self, _ids: &[i64], _h: &[f32]) -> Result<Vec<f32>> {
let mut logits = vec![0.0f32; 300];
logits[100] = 1.0;
Ok(logits)
}
}
let mut engine = NeverEos;
let ids = greedy_generate(&mut engine, "x", 10).unwrap();
assert_eq!(ids.len(), 10);
assert!(ids.iter().all(|&t| t == 100));
}
#[test]
fn fixture_byte_rule_and_argmax_tiebreak() {
assert_eq!(ids_to_fixture_bytes(&[97, 2, 0, 101]), b"^\x00\x00b");
assert_eq!(crate::imf::tokens::decode(&[97, 2, 0, 101]), "^b");
assert_eq!(argmax(&[1.0, 3.0, 3.0]), Some(1)); assert_eq!(argmax(&[]), None);
}
}