1use serde::{Deserialize, Serialize};
11use std::sync::Mutex;
12use std::time::{Instant, SystemTime};
13
14use crate::error::{FurnaceError, ModelError, Result};
15use crate::tensor::{DataType, HostTensor, IoSpec, TensorInfo, TensorSpec, Values};
16
17#[cfg(any(test, feature = "test-models"))]
18mod testing;
19
20#[cfg(any(test, feature = "test-models"))]
21pub use testing::{DummyModel, DummyTensorsModel};
22
23pub const MAX_BATCH_SIZE: usize = 1000;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ModelInfo {
29 pub name: String,
31 pub version: String,
32 pub input_spec: TensorSpec,
34 pub output_spec: TensorSpec,
36 pub inputs: Vec<TensorInfo>,
38 pub outputs: Vec<TensorInfo>,
40 pub model_type: String,
42 pub backend: String,
44 pub created_at: String,
46 pub model_size_bytes: u64,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ModelStats {
56 pub inference_count: u64,
58 pub total_inference_time_ms: f64,
60 #[serde(with = "crate::time::serde_utc")]
62 pub last_inference_time: Option<SystemTime>,
63 pub average_inference_time_ms: f64,
65 pub error_count: u64,
67 pub success_count: u64,
69 pub min_inference_time_ms: Option<f64>,
71 pub max_inference_time_ms: Option<f64>,
73 pub batch_count: u64,
75 pub batched_items: u64,
77}
78
79impl ModelStats {
80 fn new() -> Self {
81 Self {
82 inference_count: 0,
83 total_inference_time_ms: 0.0,
84 last_inference_time: None,
85 average_inference_time_ms: 0.0,
86 error_count: 0,
87 success_count: 0,
88 min_inference_time_ms: None,
89 max_inference_time_ms: None,
90 batch_count: 0,
91 batched_items: 0,
92 }
93 }
94
95 fn record_success(&mut self, elapsed_ms: f64, now: SystemTime) {
97 self.inference_count += 1;
98 self.success_count += 1;
99 self.total_inference_time_ms += elapsed_ms;
100 self.average_inference_time_ms = self.total_inference_time_ms / self.success_count as f64;
101 self.min_inference_time_ms = Some(
102 self.min_inference_time_ms
103 .map_or(elapsed_ms, |m| m.min(elapsed_ms)),
104 );
105 self.max_inference_time_ms = Some(
106 self.max_inference_time_ms
107 .map_or(elapsed_ms, |m| m.max(elapsed_ms)),
108 );
109 self.last_inference_time = Some(now);
110 }
111
112 fn record_error(&mut self) {
113 self.inference_count += 1;
114 self.error_count += 1;
115 }
116
117 fn record_batch(&mut self, items: usize) {
118 self.batch_count += 1;
119 self.batched_items += items as u64;
120 }
121}
122
123#[derive(Debug)]
125pub struct FlatInput {
126 pub data: Vec<f32>,
128 pub batch_size: usize,
130}
131
132pub trait TensorModel: Send + Sync + std::fmt::Debug {
165 fn name(&self) -> &str;
167
168 fn backend(&self) -> &str;
170
171 fn inputs(&self) -> Vec<IoSpec>;
173
174 fn outputs(&self) -> Vec<IoSpec>;
176
177 fn run(&self, inputs: Vec<HostTensor>) -> Result<Vec<HostTensor>>;
182
183 fn fixed_batch(&self) -> Option<usize> {
186 None
187 }
188
189 fn int_range(&self) -> (i64, i64) {
192 (i64::MIN, i64::MAX)
193 }
194}
195
196#[derive(Debug)]
203pub struct Model {
204 inner: Box<dyn TensorModel>,
205 info: ModelInfo,
206 model_type: &'static str,
207 inputs: Vec<IoSpec>,
208 outputs: Vec<IoSpec>,
209 simple: bool,
211 int_range: (i64, i64),
213 stats: std::sync::Arc<Mutex<ModelStats>>,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221enum Door {
222 Named,
223 Flat,
224}
225
226fn tensor_spec(spec: Option<&IoSpec>) -> TensorSpec {
228 TensorSpec {
229 shape: spec
230 .map(|s| s.shape.iter().map(|d| d.unwrap_or(0)).collect())
231 .unwrap_or_default(),
232 dtype: match spec.map(|s| s.dtype) {
233 Some(DataType::Int64) => "int64",
234 Some(DataType::Int32) => "int32",
235 _ => "float32",
236 }
237 .to_string(),
238 min_value: None,
239 max_value: None,
240 }
241}
242
243impl Model {
244 pub fn custom(model: impl TensorModel + 'static) -> Self {
246 Self::new(Box::new(model), "custom", 0)
247 }
248
249 pub fn new(
251 inner: Box<dyn TensorModel>,
252 model_type: &'static str,
253 model_size_bytes: u64,
254 ) -> Self {
255 let inputs = inner.inputs();
256 let outputs = inner.outputs();
257 let int_range = inner.int_range();
258 let simple = match (inputs.as_slice(), outputs.as_slice()) {
259 ([input], [output]) => [input, output].iter().all(|spec| {
260 spec.dtype == DataType::Fp32
261 && spec.static_shape().is_some_and(|shape| !shape.is_empty())
262 }),
263 _ => false,
264 };
265 let info = ModelInfo {
266 name: inner.name().to_string(),
267 version: "1.0.0".to_string(),
268 input_spec: tensor_spec(inputs.first()),
269 output_spec: tensor_spec(outputs.first()),
270 inputs: inputs.iter().map(TensorInfo::from).collect(),
271 outputs: outputs.iter().map(TensorInfo::from).collect(),
272 model_type: model_type.to_string(),
273 backend: inner.backend().to_string(),
274 created_at: crate::time::rfc3339_now(),
275 model_size_bytes,
276 };
277
278 Self {
279 inner,
280 info,
281 model_type,
282 int_range,
283 inputs,
284 outputs,
285 simple,
286 stats: std::sync::Arc::new(Mutex::new(ModelStats::new())),
287 }
288 }
289
290 pub fn batches_named(&self) -> bool {
293 !self.simple && self.inputs.iter().all(|spec| spec.static_shape().is_some())
294 }
295
296 pub fn is_simple(&self) -> bool {
300 self.simple
301 }
302
303 pub fn inputs(&self) -> &[IoSpec] {
305 &self.inputs
306 }
307
308 pub fn outputs(&self) -> &[IoSpec] {
310 &self.outputs
311 }
312
313 pub fn infer(&self, inputs: Vec<HostTensor>) -> Result<Vec<HostTensor>> {
317 self.infer_many(vec![inputs])
318 .pop()
319 .expect("one request yields one result")
320 }
321
322 pub fn infer_many(&self, requests: Vec<Vec<HostTensor>>) -> Vec<Result<Vec<HostTensor>>> {
326 self.run_requests(requests.into_iter().map(Ok).collect(), Door::Named)
327 }
328
329 fn run_requests(
332 &self,
333 requests: Vec<Result<Vec<HostTensor>>>,
334 door: Door,
335 ) -> Vec<Result<Vec<HostTensor>>> {
336 let mut results: Vec<Option<Result<Vec<HostTensor>>>> =
337 (0..requests.len()).map(|_| None).collect();
338 let mut valid = Vec::with_capacity(requests.len());
339 let mut errors = 0;
340 for (i, inputs) in requests.into_iter().enumerate() {
341 let checked = inputs.and_then(|inputs| {
342 self.check_inputs(&inputs, door)
343 .map(|batch| (inputs, batch))
344 });
345 match checked {
346 Ok((inputs, batch)) => valid.push((i, inputs, batch)),
347 Err(e) => {
348 results[i] = Some(Err(e));
349 errors += 1;
350 }
351 }
352 }
353
354 let start = Instant::now();
355 let total: usize = valid.iter().map(|(_, _, batch)| batch).sum();
356 let batches: Vec<(usize, usize)> = valid.iter().map(|(i, _, b)| (*i, *b)).collect();
357 let output = match valid.len() {
358 0 => None,
359 1 => {
360 let (_, inputs, _) = valid.pop().expect("one valid request");
361 Some(self.run_checked(inputs, total))
362 }
363 _ => Some(
364 join_requests(valid.into_iter().map(|(_, inputs, _)| inputs).collect())
365 .and_then(|inputs| self.run_checked(inputs, total)),
366 ),
367 };
368 let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
369 let now = SystemTime::now();
370
371 let mut stats = self.stats.lock().unwrap_or_else(|e| e.into_inner());
372 for _ in 0..errors {
373 stats.record_error();
374 }
375 if let Some(output) = output {
376 stats.record_batch(total);
377 match output {
378 Ok(outputs) if batches.len() == 1 => {
379 stats.record_success(elapsed_ms, now);
380 results[batches[0].0] = Some(Ok(outputs));
381 }
382 Ok(outputs) => {
383 let mut offset = 0;
384 for (i, batch) in batches {
385 let mine = outputs
386 .iter()
387 .map(|output| output.slice_batch(offset, offset + batch))
388 .collect();
389 offset += batch;
390 stats.record_success(elapsed_ms, now);
391 results[i] = Some(Ok(mine));
392 }
393 }
394 Err(e) if batches.len() == 1 => {
396 stats.record_error();
397 results[batches[0].0] = Some(Err(e));
398 }
399 Err(e) => {
400 for (i, _) in batches {
401 stats.record_error();
402 results[i] = Some(Err(duplicate(&e)));
403 }
404 }
405 }
406 }
407 results
408 .into_iter()
409 .map(|r| r.expect("every request has a result"))
410 .collect()
411 }
412
413 fn run_checked(&self, inputs: Vec<HostTensor>, batch: usize) -> Result<Vec<HostTensor>> {
414 let outputs = self.run_in_chunks(inputs, batch)?;
415 self.check_outputs(&outputs, batch)?;
416 Ok(outputs)
417 }
418
419 fn check_inputs(&self, inputs: &[HostTensor], door: Door) -> Result<usize> {
421 let invalid =
422 |message: String| -> FurnaceError { ModelError::InvalidInput(message).into() };
423 if inputs.len() != self.inputs.len() {
424 return Err(invalid(format!(
425 "expected {} inputs ({}), got {}",
426 self.inputs.len(),
427 self.inputs
428 .iter()
429 .map(|i| i.name.as_str())
430 .collect::<Vec<_>>()
431 .join(", "),
432 inputs.len()
433 )));
434 }
435 let mut batch = None;
436 for (spec, tensor) in self.inputs.iter().zip(inputs) {
437 let name = &spec.name;
438 match (&tensor.values, spec.dtype.is_int()) {
439 (Values::F32(values), false) => {
440 if let Some(pos) = values.iter().position(|v| !v.is_finite()) {
441 if door == Door::Flat {
442 let per_item: usize = tensor.shape.iter().skip(1).product();
443 return Err(ModelError::InvalidDataType(format!(
444 "Invalid value at batch[{}][{}]: {} (not finite)",
445 pos / per_item,
446 pos % per_item,
447 values[pos]
448 ))
449 .into());
450 }
451 return Err(invalid(format!(
452 "'{name}' has a non-finite value at index {pos}"
453 )));
454 }
455 }
456 (Values::I64(values), true) => {
457 let (min, max) = self.int_range;
458 if let Some(pos) = values.iter().position(|v| !(min..=max).contains(v)) {
459 return Err(invalid(format!(
460 "'{name}' has {} at index {pos}, outside {min}..={max}, the range \
461 of this backend's integers",
462 values[pos]
463 )));
464 }
465 }
466 _ => {
467 return Err(invalid(format!(
468 "'{name}' must be {}",
469 spec.dtype.v2_name()
470 )));
471 }
472 }
473 if tensor.shape.is_empty() {
476 return Err(invalid(format!(
477 "'{name}' has no batch dimension: its shape is [], the model takes {:?}",
478 spec.v2_shape()
479 )));
480 }
481 let item = &tensor.shape[1..];
482 let fits = item.len() == spec.shape.len()
483 && item
484 .iter()
485 .zip(&spec.shape)
486 .all(|(&d, want)| want.is_none_or(|w| w == d) && d > 0);
487 if !fits {
488 return Err(invalid(format!(
489 "'{name}' has shape {:?}; the model takes {:?} (-1: any)",
490 tensor.shape,
491 spec.v2_shape()
492 )));
493 }
494 let expected_len: usize = tensor.shape.iter().product();
495 if tensor.values.len() != expected_len {
496 return Err(invalid(format!(
497 "'{name}' has shape {:?} ({expected_len} values) but {} values",
498 tensor.shape,
499 tensor.values.len()
500 )));
501 }
502 match batch {
503 None => batch = Some(tensor.batch()),
504 Some(b) if b != tensor.batch() => {
505 return Err(invalid(format!(
506 "inputs disagree on the batch size: {b} and {} ('{name}')",
507 tensor.batch()
508 )));
509 }
510 Some(_) => {}
511 }
512 }
513 let batch = batch.unwrap_or(0);
514 Self::check_batch_size(batch)?;
515 Ok(batch)
516 }
517
518 fn run_in_chunks(&self, inputs: Vec<HostTensor>, batch: usize) -> Result<Vec<HostTensor>> {
520 let chunk = match self.inner.fixed_batch() {
521 Some(chunk) if chunk != batch => chunk,
522 _ => return self.inner.run(inputs),
523 };
524 if !batch.is_multiple_of(chunk) {
525 return Err(ModelError::InvalidInput(format!(
526 "the model runs batches of {chunk}; got {batch} items"
527 ))
528 .into());
529 }
530 let mut per_output: Vec<Vec<HostTensor>> = Vec::new();
531 for start in (0..batch).step_by(chunk) {
532 let part = inputs
533 .iter()
534 .map(|t| t.slice_batch(start, start + chunk))
535 .collect();
536 for (i, output) in self.inner.run(part)?.into_iter().enumerate() {
537 if per_output.len() <= i {
538 per_output.push(Vec::new());
539 }
540 per_output[i].push(output);
541 }
542 }
543 per_output
544 .into_iter()
545 .map(HostTensor::concat_batch)
546 .collect()
547 }
548
549 fn check_outputs(&self, outputs: &[HostTensor], batch: usize) -> Result<()> {
550 if outputs.len() != self.outputs.len() {
551 return Err(ModelError::InferenceFailed(format!(
552 "model returned {} outputs, expected {}",
553 outputs.len(),
554 self.outputs.len()
555 ))
556 .into());
557 }
558 if let Some(output) = outputs.iter().find(|o| o.batch() != batch) {
559 return Err(ModelError::InferenceFailed(format!(
560 "model returned shape {:?} for a batch of {batch}",
561 output.shape
562 ))
563 .into());
564 }
565 Ok(())
566 }
567
568 pub fn predict(&self, input: Vec<f32>) -> Result<Vec<f32>> {
571 self.predict_batch(vec![input])
572 .map(|mut batch| batch.pop().expect("batch of one yields one output"))
573 }
574
575 pub fn predict_batch(&self, inputs: Vec<Vec<f32>>) -> Result<Vec<Vec<f32>>> {
580 let batch_size = inputs.len();
581 let flattened = match self.flatten_batch(inputs) {
582 Ok(flattened) => flattened,
583 Err(e) => {
584 self.stats
585 .lock()
586 .unwrap_or_else(|e| e.into_inner())
587 .record_error();
588 return Err(e);
589 }
590 };
591
592 let output_size: usize = self.info.output_spec.shape.iter().product();
593 Ok(self
594 .predict_flat(flattened, batch_size)?
595 .chunks_exact(output_size)
596 .map(<[f32]>::to_vec)
597 .collect())
598 }
599
600 pub fn predict_flat(&self, input: Vec<f32>, batch_size: usize) -> Result<Vec<f32>> {
606 self.predict_many(vec![FlatInput {
607 data: input,
608 batch_size,
609 }])
610 .pop()
611 .expect("one request yields one result")
612 }
613
614 pub fn predict_many(&self, requests: Vec<FlatInput>) -> Vec<Result<Vec<f32>>> {
622 let batches: Vec<usize> = requests.iter().map(|r| r.batch_size).collect();
623 let requests = requests.into_iter().map(|r| self.flat_request(r)).collect();
624 self.run_requests(requests, Door::Flat)
625 .into_iter()
626 .zip(batches)
627 .map(|(result, batch)| result.and_then(|outputs| self.flat_output(outputs, batch)))
628 .collect()
629 }
630
631 fn flat_request(&self, FlatInput { data, batch_size }: FlatInput) -> Result<Vec<HostTensor>> {
634 Self::check_batch_size(batch_size)?;
635 let item = &self.info.input_spec.shape;
636 if data.len() != batch_size * item.iter().product::<usize>() {
637 return Err(ModelError::InputValidation {
638 batch: batch_size,
639 item: item.clone(),
640 got: data.len(),
641 }
642 .into());
643 }
644 let mut shape = Vec::with_capacity(item.len() + 1);
645 shape.push(batch_size);
646 shape.extend_from_slice(item);
647 Ok(vec![HostTensor {
648 shape,
649 values: Values::F32(data),
650 }])
651 }
652
653 fn flat_output(&self, outputs: Vec<HostTensor>, batch_size: usize) -> Result<Vec<f32>> {
655 let output_size: usize = self.info.output_spec.shape.iter().product();
656 match outputs.into_iter().next() {
657 Some(HostTensor {
658 values: Values::F32(values),
659 ..
660 }) if values.len() == batch_size * output_size => Ok(values),
661 Some(output) => Err(ModelError::InferenceFailed(format!(
662 "model returned shape {:?}, expected [{batch_size}, ..{:?}]",
663 output.shape, self.info.output_spec.shape
664 ))
665 .into()),
666 None => Err(ModelError::InferenceFailed("model returned no output".to_string()).into()),
667 }
668 }
669
670 fn check_batch_size(batch_size: usize) -> Result<()> {
671 if batch_size == 0 {
672 return Err(ModelError::EmptyBatch.into());
673 }
674 if batch_size > MAX_BATCH_SIZE {
675 return Err(ModelError::BatchTooLarge {
676 size: batch_size,
677 max: MAX_BATCH_SIZE,
678 }
679 .into());
680 }
681 Ok(())
682 }
683
684 fn flatten_batch(&self, inputs: Vec<Vec<f32>>) -> Result<Vec<f32>> {
685 Self::check_batch_size(inputs.len())?;
686 let input_size: usize = self.info.input_spec.shape.iter().product();
687 let mut flattened = Vec::with_capacity(inputs.len() * input_size);
688 for input in inputs {
689 self.validate_input_shape(&input)?;
690 flattened.extend_from_slice(&input);
691 }
692 Ok(flattened)
693 }
694
695 pub fn adopt_stats(&mut self, previous: &Model) {
698 self.stats = previous.stats.clone();
699 }
700
701 pub fn warm_up(&self) -> Result<Option<std::time::Duration>> {
708 let start = Instant::now();
709 let batch = self.inner.fixed_batch().unwrap_or(1);
711 let Some(inputs) = self
712 .inputs
713 .iter()
714 .map(|spec| {
715 let mut shape = vec![batch];
716 shape.extend(spec.static_shape()?);
717 let len = shape.iter().product();
718 let values = if spec.dtype.is_int() {
719 Values::I64(vec![0; len])
720 } else {
721 Values::F32(vec![0.0; len])
722 };
723 Some(HostTensor { shape, values })
724 })
725 .collect::<Option<Vec<_>>>()
726 else {
727 return Ok(None);
728 };
729 self.inner.run(inputs)?;
730 Ok(Some(start.elapsed()))
731 }
732
733 pub fn get_info(&self) -> &ModelInfo {
735 &self.info
736 }
737
738 pub fn get_stats(&self) -> ModelStats {
740 self.stats.lock().unwrap_or_else(|e| e.into_inner()).clone()
741 }
742
743 pub fn validate_input_shape(&self, input: &[f32]) -> Result<()> {
745 let expected_size: usize = self.info.input_spec.shape.iter().product();
746 if input.len() != expected_size {
747 return Err(ModelError::InputValidation {
748 batch: 1,
749 item: self.info.input_spec.shape.clone(),
750 got: input.len(),
751 }
752 .into());
753 }
754 Ok(())
755 }
756
757 pub fn get_backend_info(&self) -> String {
759 self.info.backend.clone()
760 }
761
762 pub fn model_type(&self) -> &'static str {
764 self.model_type
765 }
766}
767
768fn join_requests(requests: Vec<Vec<HostTensor>>) -> Result<Vec<HostTensor>> {
770 let count = requests.first().map_or(0, Vec::len);
771 let mut columns: Vec<Vec<HostTensor>> = (0..count).map(|_| Vec::new()).collect();
772 for inputs in requests {
773 for (column, tensor) in columns.iter_mut().zip(inputs) {
774 column.push(tensor);
775 }
776 }
777 columns.into_iter().map(HostTensor::concat_batch).collect()
778}
779
780fn duplicate(error: &FurnaceError) -> FurnaceError {
783 let FurnaceError::Model(error) = error else {
784 return ModelError::InferenceFailed(error.to_string()).into();
785 };
786 match error {
787 ModelError::LoadFailed { path, source } => ModelError::LoadFailed {
788 path: path.clone(),
789 source: source.to_string().into(),
790 },
791 ModelError::InvalidFormat(m) => ModelError::InvalidFormat(m.clone()),
792 ModelError::FileNotFound(path) => ModelError::FileNotFound(path.clone()),
793 ModelError::InferenceFailed(m) => ModelError::InferenceFailed(m.clone()),
794 ModelError::InputValidation { batch, item, got } => ModelError::InputValidation {
795 batch: *batch,
796 item: item.clone(),
797 got: *got,
798 },
799 ModelError::EmptyBatch => ModelError::EmptyBatch,
800 ModelError::BatchTooLarge { size, max } => ModelError::BatchTooLarge {
801 size: *size,
802 max: *max,
803 },
804 ModelError::InvalidDataType(m) => ModelError::InvalidDataType(m.clone()),
805 ModelError::InvalidInput(m) => ModelError::InvalidInput(m.clone()),
806 ModelError::InvalidArgument { arg, value, reason } => ModelError::InvalidArgument {
807 arg: arg.clone(),
808 value: value.clone(),
809 reason: reason.clone(),
810 },
811 }
812 .into()
813}
814
815#[cfg(test)]
816mod tests {
817 use super::*;
818
819 fn get_test_model() -> Model {
820 let dummy = DummyModel::new("test".to_string(), vec![784], vec![10]);
821 Model::new(Box::new(dummy), "dummy", 0)
822 }
823
824 fn tensors(ids: Vec<i64>, scale: Vec<f32>) -> Vec<HostTensor> {
825 vec![
826 HostTensor {
827 shape: vec![scale.len(), 4],
828 values: Values::I64(ids),
829 },
830 HostTensor {
831 shape: vec![scale.len()],
832 values: Values::F32(scale),
833 },
834 ]
835 }
836
837 #[test]
838 fn named_tensors_in_and_out() {
839 let model = Model::new(Box::new(DummyTensorsModel::new(None)), "dummy", 0);
840 assert!(!model.is_simple());
841 let info = model.get_info();
842 assert_eq!(info.inputs[0].datatype, "INT64");
843 assert_eq!(info.inputs[0].shape, [-1, 4]);
844 assert_eq!(info.inputs[1].shape, [-1]);
845 assert_eq!(info.input_spec.dtype, "int64");
846
847 let outputs = model
848 .infer(tensors(vec![1, 2, 3, 4, 5, 6, 7, 8], vec![0.5, 2.0]))
849 .unwrap();
850 assert_eq!(outputs[0].shape, [2, 4]);
851 assert_eq!(
852 outputs[0].values,
853 Values::F32(vec![0.5, 1.0, 1.5, 2.0, 10.0, 12.0, 14.0, 16.0])
854 );
855 assert_eq!(outputs[1].values, Values::I64(vec![10, 26]));
856 assert_eq!(model.get_stats().success_count, 1);
857 assert!(model.warm_up().unwrap().is_some());
859 }
860
861 #[test]
862 fn named_tensors_are_checked() {
863 let model = Model::new(Box::new(DummyTensorsModel::new(None)), "dummy", 0);
864 let check = |inputs: Vec<HostTensor>, expected: &str| {
865 let err = model.infer(inputs).unwrap_err().to_string();
866 assert!(err.contains(expected), "{expected:?} not in {err:?}");
867 };
868 let mut inputs = tensors(vec![1, 2, 3, 4], vec![1.0]);
869 inputs.pop();
870 check(inputs, "expected 2 inputs (ids, scale)");
871 let mut swapped = tensors(vec![1, 2, 3, 4], vec![1.0]);
872 swapped.swap(0, 1);
873 check(swapped, "'ids' must be INT64");
874 let mut short = tensors(vec![1, 2, 3], vec![1.0]);
875 short[0].shape = vec![1, 3];
876 check(short, "'ids' has shape [1, 3]");
877 check(
878 tensors(vec![1, 2, 3, 4, 5, 6, 7, 8], vec![1.0]),
879 "'ids' has shape [1, 4] (4 values) but 8 values",
880 );
881 let mut disagree = tensors(vec![1, 2, 3, 4, 5, 6, 7, 8], vec![1.0, 2.0]);
882 disagree[1] = HostTensor {
883 shape: vec![1],
884 values: Values::F32(vec![1.0]),
885 };
886 check(disagree, "disagree on the batch size");
887 check(
888 tensors(vec![1, 2, 3, 4], vec![f32::NAN]),
889 "'scale' has a non-finite value",
890 );
891 assert_eq!(model.get_stats().error_count, 6);
892 }
893
894 #[test]
895 fn requests_of_named_tensors_share_a_forward_pass() {
896 let model = Model::new(Box::new(DummyTensorsModel::new(None)), "dummy", 0);
897 assert!(model.batches_named());
898 let mut bad_shape = tensors(vec![1, 2, 3, 4], vec![1.0]);
899 bad_shape[0].shape = vec![2, 2];
900 let results = model.infer_many(vec![
901 tensors(vec![1, 2, 3, 4], vec![1.0]),
902 bad_shape,
903 tensors(vec![1, 1, 1, 1, 2, 2, 2, 2], vec![2.0, 0.5]),
904 tensors(vec![i64::from(i32::MAX) + 1, 0, 0, 0], vec![1.0]),
905 ]);
906 let first = results[0].as_ref().unwrap();
908 assert_eq!(first[0].shape, [1, 4]);
909 assert_eq!(first[1].values, Values::I64(vec![10]));
910 let third = results[2].as_ref().unwrap();
911 assert_eq!(third[0].shape, [2, 4]);
912 assert_eq!(
913 third[0].values,
914 Values::F32(vec![2.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0])
915 );
916 assert_eq!(third[1].values, Values::I64(vec![4, 8]));
917 assert!(
919 results[1]
920 .as_ref()
921 .unwrap_err()
922 .to_string()
923 .contains("'ids'")
924 );
925 let range = results[3].as_ref().unwrap_err().to_string();
926 assert!(range.contains("outside"), "{range}");
927 let stats = model.get_stats();
928 assert_eq!((stats.batch_count, stats.batched_items), (1, 3));
929 assert_eq!((stats.success_count, stats.error_count), (2, 2));
930 }
931
932 #[test]
933 fn fixed_batch_graphs_run_in_chunks() {
934 let model = Model::new(Box::new(DummyTensorsModel::new(Some(2))), "dummy", 0);
935 let outputs = model
936 .infer(tensors((1..=24).collect(), vec![1.0; 6]))
937 .unwrap();
938 assert_eq!(outputs[0].shape, [6, 4]);
939 assert_eq!(outputs[1].values, Values::I64(vec![10, 26, 42, 58, 74, 90]));
940 let err = model
941 .infer(tensors((1..=12).collect(), vec![1.0; 3]))
942 .unwrap_err()
943 .to_string();
944 assert!(err.contains("runs batches of 2"), "{err}");
945 assert!(model.warm_up().unwrap().is_some());
947 }
948
949 #[derive(Debug)]
951 struct PairsOnly(DummyModel);
952
953 impl TensorModel for PairsOnly {
954 fn name(&self) -> &str {
955 self.0.name()
956 }
957 fn backend(&self) -> &str {
958 self.0.backend()
959 }
960 fn inputs(&self) -> Vec<IoSpec> {
961 self.0.inputs()
962 }
963 fn outputs(&self) -> Vec<IoSpec> {
964 self.0.outputs()
965 }
966 fn run(&self, inputs: Vec<HostTensor>) -> Result<Vec<HostTensor>> {
967 self.0.run(inputs)
968 }
969 fn fixed_batch(&self) -> Option<usize> {
970 Some(2)
971 }
972 }
973
974 #[test]
975 fn a_batch_the_graph_cannot_run_is_the_clients_error() {
976 let dummy = DummyModel::new("pairs".to_string(), vec![3], vec![2]);
977 let model = Model::new(Box::new(PairsOnly(dummy)), "dummy", 0);
978 let err = model
979 .predict_many(vec![FlatInput {
980 data: vec![1.0; 9],
981 batch_size: 3,
982 }])
983 .pop()
984 .unwrap()
985 .unwrap_err();
986 assert!(err.is_client_error(), "{err:?}");
987 assert!(!err.to_string().contains("Inference failed"), "{err}");
988 assert!(err.to_string().contains("runs batches of 2"), "{err}");
989 let pairs = model.predict_many(vec![FlatInput {
990 data: vec![1.0; 12],
991 batch_size: 4,
992 }]);
993 assert_eq!(pairs[0].as_ref().unwrap().len(), 8);
994 assert!(model.warm_up().unwrap().is_some());
995 }
996
997 #[test]
998 fn a_shared_failure_keeps_its_error_for_every_request() {
999 let model = Model::new(Box::new(DummyTensorsModel::new(Some(2))), "dummy", 0);
1002 let results = model.infer_many(vec![
1003 tensors(vec![1, 2, 3, 4], vec![1.0]),
1004 tensors(vec![1, 2, 3, 4, 5, 6, 7, 8], vec![1.0, 2.0]),
1005 ]);
1006 for result in results {
1007 let err = result.unwrap_err();
1008 assert!(
1009 matches!(
1010 err,
1011 crate::error::FurnaceError::Model(ModelError::InvalidInput(_))
1012 ),
1013 "{err:?}"
1014 );
1015 assert!(err.to_string().contains("runs batches of 2"), "{err}");
1016 }
1017 assert_eq!(model.get_stats().error_count, 2);
1018 }
1019
1020 #[test]
1021 fn an_input_without_a_batch_dimension_is_rejected() {
1022 let model = Model::new(Box::new(DummyTensorsModel::new(None)), "dummy", 0);
1023 let mut inputs = tensors(vec![1, 2, 3, 4], vec![1.0]);
1024 inputs[1].shape = vec![];
1025 assert_eq!(inputs[1].batch(), 0);
1026 let err = model.infer(inputs).unwrap_err().to_string();
1027 assert!(err.contains("'scale' has no batch dimension"), "{err}");
1028 }
1029
1030 #[test]
1031 fn test_model_info() {
1032 let model = get_test_model();
1033 let info = model.get_info();
1034 assert_eq!(info.model_type, "dummy");
1035 assert_eq!(info.backend, "cpu");
1036 assert_eq!(info.input_spec.shape, vec![784]);
1037 assert_eq!(info.output_spec.shape, vec![10]);
1038 assert_eq!(info.input_spec.dtype, "float32");
1039 }
1040
1041 #[test]
1042 fn test_inference_with_valid_input() {
1043 let model = get_test_model();
1044 let output = model.predict(vec![0.5; 784]).unwrap();
1045 assert_eq!(output.len(), 10);
1046 }
1047
1048 #[test]
1049 fn test_batch_inference_splits_outputs() {
1050 let model = get_test_model();
1051 let outputs = model.predict_batch(vec![vec![0.5; 784]; 3]).unwrap();
1052 assert_eq!(outputs.len(), 3);
1053 assert!(outputs.iter().all(|o| o.len() == 10));
1054 }
1055
1056 #[test]
1057 fn test_predict_flat_matches_batch() {
1058 let model = get_test_model();
1059 let flat = model.predict_flat(vec![0.5; 784 * 3], 3).unwrap();
1060 let batch = model.predict_batch(vec![vec![0.5; 784]; 3]).unwrap();
1061 assert_eq!(flat, batch.concat());
1062 assert_eq!(model.get_stats().success_count, 2);
1063 }
1064
1065 #[test]
1066 fn test_predict_flat_rejects_bad_input() {
1067 let model = get_test_model();
1068 let err = model.predict_flat(vec![0.5; 784 * 2 + 1], 2).unwrap_err();
1070 assert!(matches!(
1071 err,
1072 crate::error::FurnaceError::Model(ModelError::InputValidation { .. })
1073 ));
1074 assert_eq!(
1075 err.to_string(),
1076 "expected 1568 values (batch 2 × [784]), got 1569"
1077 );
1078 let err = model.predict_flat(vec![], 0).unwrap_err();
1079 assert!(matches!(
1080 err,
1081 crate::error::FurnaceError::Model(ModelError::EmptyBatch)
1082 ));
1083 let err = model
1084 .predict_flat(vec![0.5; 784], MAX_BATCH_SIZE + 1)
1085 .unwrap_err();
1086 assert!(matches!(
1087 err,
1088 crate::error::FurnaceError::Model(ModelError::BatchTooLarge { .. })
1089 ));
1090
1091 let mut input = vec![0.5; 784 * 2];
1093 input[784 + 5] = f32::NAN;
1094 let err = model.predict_flat(input, 2).unwrap_err().to_string();
1095 assert!(err.contains("batch[1][5]"), "{err}");
1096 assert_eq!(model.get_stats().error_count, 4);
1097 }
1098
1099 #[test]
1100 fn test_inference_with_invalid_shape() {
1101 let model = get_test_model();
1102 let result = model.predict(vec![0.5; 100]);
1103 assert!(matches!(
1104 result,
1105 Err(crate::error::FurnaceError::Model(
1106 ModelError::InputValidation { .. }
1107 ))
1108 ));
1109 }
1110
1111 #[test]
1112 fn test_inference_rejects_non_finite() {
1113 let model = get_test_model();
1114 let mut input = vec![0.5; 784];
1115 input[42] = f32::NAN;
1116 assert!(matches!(
1117 model.predict(input),
1118 Err(crate::error::FurnaceError::Model(
1119 ModelError::InvalidDataType(_)
1120 ))
1121 ));
1122 }
1123
1124 #[test]
1125 fn a_reloaded_model_shares_its_predecessors_stats() {
1126 let old = get_test_model();
1127 old.predict(vec![0.5; 784]).unwrap();
1128 let mut new = get_test_model();
1129 new.adopt_stats(&old);
1130 old.predict(vec![0.5; 784]).unwrap();
1132 new.predict(vec![0.5; 784]).unwrap();
1133 assert_eq!(new.get_stats().success_count, 3);
1134 assert_eq!(old.get_stats().success_count, 3);
1135 }
1136
1137 #[test]
1138 fn test_model_stats() {
1139 let model = get_test_model();
1140 let stats = model.get_stats();
1141 assert_eq!(stats.inference_count, 0);
1142 assert!(stats.last_inference_time.is_none());
1143
1144 model.predict(vec![0.5; 784]).unwrap();
1145 let _ = model.predict(vec![0.5; 1]);
1146
1147 let stats = model.get_stats();
1148 assert_eq!(stats.inference_count, 2);
1149 assert_eq!(stats.success_count, 1);
1150 assert_eq!(stats.error_count, 1);
1151 assert!(stats.last_inference_time.is_some());
1152 assert!(stats.min_inference_time_ms.is_some());
1153 }
1154}