Skip to main content

autoagents_onnx/runtime/
inference.rs

1//! ONNX Runtime backend for onnx inference
2
3use crate::error::{EdgeError, EdgeResult};
4use crate::runtime::{InferenceInput, InferenceOutput};
5use crate::{Device, Model};
6use serde_json::Value;
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9
10use ndarray::{ArrayD, IxDyn};
11
12use ort::execution_providers::ExecutionProvider;
13use ort::{session::Session, value::Value as OrtValue};
14
15/// ONNX Runtime backend for edge inference
16pub struct OnnxBackend {
17    session: Session,
18    input_info: Vec<InputInfo>,
19    output_info: Vec<OutputInfo>,
20}
21
22#[derive(Debug, Clone)]
23struct InputInfo {
24    name: String,
25    shape: Vec<i64>,
26    data_type: String,
27}
28
29#[derive(Debug, Clone)]
30struct OutputInfo {
31    name: String,
32    shape: Vec<i64>,
33    data_type: String,
34}
35
36impl OnnxBackend {
37    /// Create a new ONNX backend from a model with a specific device
38    pub fn from_model_with_device(model: Box<dyn Model>, device: Device) -> EdgeResult<Self> {
39        // Validate the model
40        model.validate()?;
41
42        // Check if device is available
43        if !device.is_available() {
44            return Err(EdgeError::runtime(format!(
45                "Device {device} is not available"
46            )));
47        }
48
49        // Get the actual ONNX model file path
50        let model_path = model.model_path();
51        let onnx_file = if model_path.is_file()
52            && model_path.extension().and_then(|e| e.to_str()) == Some("onnx")
53        {
54            model_path.to_path_buf()
55        } else {
56            model_path.join("model.onnx")
57        };
58
59        if !onnx_file.exists() {
60            return Err(EdgeError::model(format!(
61                "ONNX model file not found: {}",
62                onnx_file.display()
63            )));
64        }
65
66        Self::new_with_device(onnx_file, device)
67    }
68
69    /// Create a new ONNX backend from a model (uses CPU device by default)
70    pub fn from_model(model: Box<dyn Model>) -> EdgeResult<Self> {
71        let device = crate::device::cpu();
72        Self::from_model_with_device(model, device)
73    }
74
75    /// Create a new ONNX backend with a specific device
76    pub fn new_with_device<P: AsRef<Path>>(model_path: P, device: Device) -> EdgeResult<Self> {
77        // Check if device is available
78        if !device.is_available() {
79            return Err(EdgeError::runtime(format!(
80                "Device {device} is not available"
81            )));
82        }
83
84        // Create session with device-specific execution provider following USLS pattern
85        let mut builder = Session::builder()
86            .map_err(|e| EdgeError::runtime(format!("Failed to create session builder: {e}")))?;
87
88        // Register execution provider based on device type (following USLS pattern)
89        match device {
90            #[allow(unused_variables)]
91            #[cfg(feature = "cuda")]
92            Device::Cuda(id) => {
93                use ort::execution_providers::CUDAExecutionProvider;
94                let ep = CUDAExecutionProvider::default().with_device_id(id as i32);
95                match ep.is_available() {
96                    Ok(true) => {
97                        ep.register(&mut builder).map_err(|e| {
98                            EdgeError::runtime(format!("Failed to register CUDA: {e}"))
99                        })?;
100                    }
101                    _ => return Err(EdgeError::runtime("CUDA execution provider not available")),
102                }
103            }
104            Device::Cpu(_) => {
105                use ort::execution_providers::CPUExecutionProvider;
106                let ep = CPUExecutionProvider::default();
107                ep.register(&mut builder)
108                    .map_err(|e| EdgeError::runtime(format!("Failed to register CPU: {e}")))?;
109            }
110        }
111
112        let session = builder
113            .commit_from_file(model_path)
114            .map_err(|e| EdgeError::model(format!("Failed to load ONNX model: {e}")))?;
115
116        Self::create_backend(session)
117    }
118
119    /// Create a new ONNX backend (uses CPU device by default)
120    pub fn new<P: AsRef<Path>>(model_path: P) -> EdgeResult<Self> {
121        let device = crate::device::cpu();
122        Self::new_with_device(model_path, device)
123    }
124
125    /// Common backend creation logic
126    fn create_backend(session: Session) -> EdgeResult<Self> {
127        // Extract input information
128        let input_info: Vec<InputInfo> = session
129            .inputs
130            .iter()
131            .map(|input| {
132                let shape = vec![-1, -1]; // Dynamic shape for now
133
134                InputInfo {
135                    name: input.name.clone(),
136                    shape,
137                    data_type: format!("{:?}", input.input_type),
138                }
139            })
140            .collect();
141
142        // Extract output information
143        let output_info: Vec<OutputInfo> = session
144            .outputs
145            .iter()
146            .map(|output| {
147                let shape = vec![-1, -1, -1]; // Dynamic shape for now
148
149                OutputInfo {
150                    name: output.name.clone(),
151                    shape,
152                    data_type: format!("{:?}", output.output_type),
153                }
154            })
155            .collect();
156
157        log::info!(
158            "ONNX Backend initialized with {} inputs and {} outputs",
159            input_info.len(),
160            output_info.len()
161        );
162
163        for (i, input) in input_info.iter().enumerate() {
164            log::info!(
165                "  Input {}: name='{}', type={}, shape={:?}",
166                i,
167                input.name,
168                input.data_type,
169                input.shape
170            );
171        }
172
173        for (i, output) in output_info.iter().enumerate() {
174            log::info!(
175                "  Output {}: name='{}', type={}, shape={:?}",
176                i,
177                output.name,
178                output.data_type,
179                output.shape
180            );
181        }
182
183        Ok(Self {
184            session,
185            input_info,
186            output_info,
187        })
188    }
189
190    /// Convert JSON value to ONNX tensor
191    fn json_to_tensor(
192        &self,
193        name: &str,
194        data: &Value,
195    ) -> EdgeResult<ort::value::Value<ort::value::DynValueTypeMarker>> {
196        match data {
197            Value::Array(arr) => {
198                if let Ok(i64_values) = arr
199                    .iter()
200                    .map(|v| v.as_i64().ok_or("Invalid i64"))
201                    .collect::<Result<Vec<_>, _>>()
202                {
203                    let len = i64_values.len();
204                    let array = ArrayD::<i64>::from_shape_vec(IxDyn(&[1, len]), i64_values)
205                        .map_err(|e| {
206                            EdgeError::inference(format!(
207                                "Failed to create i64 tensor for {name}: {e}"
208                            ))
209                        })?;
210
211                    Ok(OrtValue::from_array(array)
212                        .map_err(|e| {
213                            EdgeError::inference(format!(
214                                "Failed to create ONNX value for {name}: {e}"
215                            ))
216                        })?
217                        .into_dyn())
218                }
219                // Try f32 array
220                else if let Ok(f32_values) = arr
221                    .iter()
222                    .map(|v| v.as_f64().map(|f| f as f32).ok_or("Invalid f32"))
223                    .collect::<Result<Vec<_>, _>>()
224                {
225                    let len = f32_values.len();
226                    let array = ArrayD::<f32>::from_shape_vec(IxDyn(&[1, len]), f32_values)
227                        .map_err(|e| {
228                            EdgeError::inference(format!(
229                                "Failed to create f32 tensor for {name}: {e}"
230                            ))
231                        })?;
232
233                    Ok(OrtValue::from_array(array)
234                        .map_err(|e| {
235                            EdgeError::inference(format!(
236                                "Failed to create ONNX value for {name}: {e}"
237                            ))
238                        })?
239                        .into_dyn())
240                } else {
241                    Err(EdgeError::inference(format!(
242                        "Unsupported data type in array for input: {name}"
243                    )))
244                }
245            }
246            _ => Err(EdgeError::inference(format!(
247                "Unsupported JSON type for input: {name}"
248            ))),
249        }
250    }
251
252    /// Convert ONNX tensor to JSON value
253    fn tensor_to_json_static(
254        tensor: &ort::value::Value<ort::value::DynValueTypeMarker>,
255    ) -> EdgeResult<Value> {
256        // Try to extract as f32 first
257        if let Ok((_, data)) = tensor.try_extract_tensor::<f32>() {
258            let values: Vec<Value> = data
259                .iter()
260                .map(|&x| {
261                    Value::Number(
262                        serde_json::Number::from_f64(x as f64)
263                            .unwrap_or(serde_json::Number::from(0)),
264                    )
265                })
266                .collect();
267            return Ok(Value::Array(values));
268        }
269
270        // Try to extract as i64
271        if let Ok((_, data)) = tensor.try_extract_tensor::<i64>() {
272            let values: Vec<Value> = data.iter().map(|&x| Value::Number(x.into())).collect();
273            return Ok(Value::Array(values));
274        }
275
276        Err(EdgeError::inference(
277            "Unsupported tensor type for output conversion",
278        ))
279    }
280
281    pub fn infer(&mut self, input: InferenceInput) -> EdgeResult<InferenceOutput> {
282        // Convert inputs to ONNX format
283        let mut onnx_inputs = HashMap::new();
284
285        for input_info in &self.input_info {
286            if let Some(data) = input.inputs.get(&input_info.name) {
287                let tensor = self.json_to_tensor(&input_info.name, data)?;
288                onnx_inputs.insert(input_info.name.clone(), tensor);
289            } else {
290                return Err(EdgeError::inference(format!(
291                    "Missing required input: {}",
292                    input_info.name
293                )));
294            }
295        }
296
297        // Run inference
298        let outputs = self
299            .session
300            .run(onnx_inputs)
301            .map_err(|e| EdgeError::inference(format!("ONNX inference failed: {e}")))?;
302
303        // Convert outputs back to JSON
304        let mut result_outputs = HashMap::new();
305        for output_info in &self.output_info {
306            if let Some(tensor) = outputs.get(&output_info.name) {
307                let json_data = Self::tensor_to_json_static(tensor)?;
308                result_outputs.insert(output_info.name.clone(), json_data);
309            }
310        }
311
312        let mut metadata = HashMap::new();
313        metadata.insert("backend".to_string(), Value::String("onnx".to_string()));
314        metadata.insert("inference_time_ms".to_string(), Value::Number(0.into())); // TODO: Add timing
315
316        Ok(InferenceOutput {
317            outputs: result_outputs,
318            metadata,
319        })
320    }
321
322    pub fn model_info(&self) -> HashMap<String, Value> {
323        let mut info = HashMap::new();
324        info.insert(
325            "backend_type".to_string(),
326            Value::String("onnx".to_string()),
327        );
328        info.insert(
329            "num_inputs".to_string(),
330            Value::Number(self.input_info.len().into()),
331        );
332        info.insert(
333            "num_outputs".to_string(),
334            Value::Number(self.output_info.len().into()),
335        );
336
337        let inputs: Vec<Value> = self
338            .input_info
339            .iter()
340            .map(|input| {
341                serde_json::json!({
342                    "name": input.name,
343                    "data_type": input.data_type,
344                    "shape": input.shape
345                })
346            })
347            .collect();
348        info.insert("inputs".to_string(), Value::Array(inputs));
349
350        let outputs: Vec<Value> = self
351            .output_info
352            .iter()
353            .map(|output| {
354                serde_json::json!({
355                    "name": output.name,
356                    "data_type": output.data_type,
357                    "shape": output.shape
358                })
359            })
360            .collect();
361        info.insert("outputs".to_string(), Value::Array(outputs));
362
363        info
364    }
365
366    pub fn is_ready(&self) -> bool {
367        true // ONNX session is ready once created
368    }
369
370    pub fn backend_info(&self) -> HashMap<String, Value> {
371        let mut info = HashMap::new();
372        info.insert(
373            "name".to_string(),
374            Value::String("ONNX Runtime".to_string()),
375        );
376        info.insert("version".to_string(), Value::String("2.0".to_string()));
377        info.insert("supports_gpu".to_string(), Value::Bool(false)); // TODO: Detect GPU support
378        info
379    }
380}
381
382/// ONNX model implementation
383#[derive(Debug, Clone)]
384pub struct OnnxModel {
385    path: PathBuf,
386    metadata: HashMap<String, Value>,
387}
388
389impl OnnxModel {
390    /// Create a new ONNX model from a directory path
391    pub fn from_directory<P: AsRef<Path>>(path: P) -> EdgeResult<Self> {
392        let path = path.as_ref().to_path_buf();
393        let mut metadata = HashMap::new();
394
395        // Check if path exists
396        if !path.exists() {
397            return Err(EdgeError::model(format!(
398                "Model directory does not exist: {}",
399                path.display()
400            )));
401        }
402
403        // Try to load config.json if it exists
404        let config_path = path.join("config.json");
405        if config_path.exists() {
406            let config_content = std::fs::read_to_string(&config_path)?;
407            let config: Value = serde_json::from_str(&config_content)?;
408
409            // Extract metadata from config
410            if let Some(model_type) = config.get("model_type").and_then(|v| v.as_str()) {
411                metadata.insert(
412                    "model_type".to_string(),
413                    Value::String(model_type.to_string()),
414                );
415            }
416
417            // Add other relevant config fields to metadata
418            if let Some(vocab_size) = config.get("vocab_size") {
419                metadata.insert("vocab_size".to_string(), vocab_size.clone());
420            }
421            if let Some(hidden_size) = config.get("hidden_size") {
422                metadata.insert("hidden_size".to_string(), hidden_size.clone());
423            }
424            if let Some(max_position_embeddings) = config.get("max_position_embeddings") {
425                metadata.insert(
426                    "max_position_embeddings".to_string(),
427                    max_position_embeddings.clone(),
428                );
429            }
430            if let Some(bos_token_id) = config.get("bos_token_id") {
431                metadata.insert("bos_token_id".to_string(), bos_token_id.clone());
432            }
433            if let Some(eos_token_id) = config.get("eos_token_id") {
434                metadata.insert("eos_token_id".to_string(), eos_token_id.clone());
435            }
436            if let Some(pad_token_id) = config.get("pad_token_id") {
437                metadata.insert("pad_token_id".to_string(), pad_token_id.clone());
438            }
439        }
440
441        // Add model format info
442        metadata.insert("format".to_string(), Value::String("onnx".to_string()));
443        metadata.insert(
444            "path".to_string(),
445            Value::String(path.display().to_string()),
446        );
447
448        Ok(Self { path, metadata })
449    }
450
451    /// Create a new ONNX model from a single .onnx file
452    pub fn from_file<P: AsRef<Path>>(path: P) -> EdgeResult<Self> {
453        let path = path.as_ref().to_path_buf();
454
455        if !path.exists() {
456            return Err(EdgeError::model(format!(
457                "Model file does not exist: {}",
458                path.display()
459            )));
460        }
461
462        if path.extension().and_then(|e| e.to_str()) != Some("onnx") {
463            return Err(EdgeError::model("File must have .onnx extension"));
464        }
465
466        let mut metadata = HashMap::new();
467        metadata.insert("format".to_string(), Value::String("onnx".to_string()));
468        metadata.insert(
469            "path".to_string(),
470            Value::String(path.display().to_string()),
471        );
472
473        Ok(Self { path, metadata })
474    }
475
476    /// Add metadata to the model
477    pub fn with_metadata(mut self, key: String, value: Value) -> Self {
478        self.metadata.insert(key, value);
479        self
480    }
481}
482
483impl Model for OnnxModel {
484    fn model_type(&self) -> &str {
485        "onnx"
486    }
487
488    fn model_path(&self) -> &Path {
489        &self.path
490    }
491
492    fn metadata(&self) -> &HashMap<String, Value> {
493        &self.metadata
494    }
495
496    fn config(&self) -> EdgeResult<Value> {
497        let config_path = self.path.join("config.json");
498        if config_path.exists() {
499            let config_content = std::fs::read_to_string(&config_path)?;
500            let config: Value = serde_json::from_str(&config_content)?;
501            Ok(config)
502        } else {
503            // Return basic config if no config.json exists
504            Ok(serde_json::json!({
505                "model_type": "onnx",
506                "path": self.path.display().to_string()
507            }))
508        }
509    }
510
511    fn validate(&self) -> EdgeResult<()> {
512        if !self.path.exists() {
513            return Err(EdgeError::model(format!(
514                "Model path does not exist: {}",
515                self.path.display()
516            )));
517        }
518
519        // Check for ONNX model file
520        let onnx_file = if self.path.is_file() {
521            // Direct .onnx file
522            self.path.clone()
523        } else {
524            // Directory containing model.onnx
525            self.path.join("model.onnx")
526        };
527
528        if !onnx_file.exists() {
529            return Err(EdgeError::model(format!(
530                "ONNX model file not found: {}",
531                onnx_file.display()
532            )));
533        }
534
535        Ok(())
536    }
537}
538
539/// Builder for creating models
540pub struct ModelBuilder;
541
542impl ModelBuilder {
543    /// Create an ONNX model from a directory
544    pub fn onnx_from_directory<P: AsRef<Path>>(path: P) -> EdgeResult<OnnxModel> {
545        OnnxModel::from_directory(path)
546    }
547
548    /// Create an ONNX model from a file
549    pub fn onnx_from_file<P: AsRef<Path>>(path: P) -> EdgeResult<OnnxModel> {
550        OnnxModel::from_file(path)
551    }
552}