1use 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
15pub 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 pub fn from_model_with_device(model: Box<dyn Model>, device: Device) -> EdgeResult<Self> {
39 model.validate()?;
41
42 if !device.is_available() {
44 return Err(EdgeError::runtime(format!(
45 "Device {device} is not available"
46 )));
47 }
48
49 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 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 pub fn new_with_device<P: AsRef<Path>>(model_path: P, device: Device) -> EdgeResult<Self> {
77 if !device.is_available() {
79 return Err(EdgeError::runtime(format!(
80 "Device {device} is not available"
81 )));
82 }
83
84 let mut builder = Session::builder()
86 .map_err(|e| EdgeError::runtime(format!("Failed to create session builder: {e}")))?;
87
88 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 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 fn create_backend(session: Session) -> EdgeResult<Self> {
127 let input_info: Vec<InputInfo> = session
129 .inputs
130 .iter()
131 .map(|input| {
132 let shape = vec![-1, -1]; InputInfo {
135 name: input.name.clone(),
136 shape,
137 data_type: format!("{:?}", input.input_type),
138 }
139 })
140 .collect();
141
142 let output_info: Vec<OutputInfo> = session
144 .outputs
145 .iter()
146 .map(|output| {
147 let shape = vec![-1, -1, -1]; 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 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 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 fn tensor_to_json_static(
254 tensor: &ort::value::Value<ort::value::DynValueTypeMarker>,
255 ) -> EdgeResult<Value> {
256 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 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 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 let outputs = self
299 .session
300 .run(onnx_inputs)
301 .map_err(|e| EdgeError::inference(format!("ONNX inference failed: {e}")))?;
302
303 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())); 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 }
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)); info
379 }
380}
381
382#[derive(Debug, Clone)]
384pub struct OnnxModel {
385 path: PathBuf,
386 metadata: HashMap<String, Value>,
387}
388
389impl OnnxModel {
390 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 if !path.exists() {
397 return Err(EdgeError::model(format!(
398 "Model directory does not exist: {}",
399 path.display()
400 )));
401 }
402
403 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 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 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 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 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 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 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 let onnx_file = if self.path.is_file() {
521 self.path.clone()
523 } else {
524 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
539pub struct ModelBuilder;
541
542impl ModelBuilder {
543 pub fn onnx_from_directory<P: AsRef<Path>>(path: P) -> EdgeResult<OnnxModel> {
545 OnnxModel::from_directory(path)
546 }
547
548 pub fn onnx_from_file<P: AsRef<Path>>(path: P) -> EdgeResult<OnnxModel> {
550 OnnxModel::from_file(path)
551 }
552}