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, element_count};
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 Some(expected_len) = element_count(&tensor.shape) else {
496 return Err(invalid(format!(
497 "'{name}' has shape {:?}: too many elements",
498 tensor.shape
499 )));
500 };
501 if tensor.values.len() != expected_len {
502 return Err(invalid(format!(
503 "'{name}' has shape {:?} ({expected_len} values) but {} values",
504 tensor.shape,
505 tensor.values.len()
506 )));
507 }
508 match batch {
509 None => batch = Some(tensor.batch()),
510 Some(b) if b != tensor.batch() => {
511 return Err(invalid(format!(
512 "inputs disagree on the batch size: {b} and {} ('{name}')",
513 tensor.batch()
514 )));
515 }
516 Some(_) => {}
517 }
518 }
519 let batch = batch.unwrap_or(0);
520 Self::check_batch_size(batch)?;
521 Ok(batch)
522 }
523
524 fn run_in_chunks(&self, inputs: Vec<HostTensor>, batch: usize) -> Result<Vec<HostTensor>> {
526 let chunk = match self.inner.fixed_batch() {
527 Some(chunk) if chunk != batch => chunk,
528 _ => return self.inner.run(inputs),
529 };
530 if !batch.is_multiple_of(chunk) {
531 return Err(ModelError::InvalidInput(format!(
532 "the model runs batches of {chunk}; got {batch} items"
533 ))
534 .into());
535 }
536 let mut per_output: Vec<Vec<HostTensor>> = Vec::new();
537 for start in (0..batch).step_by(chunk) {
538 let part = inputs
539 .iter()
540 .map(|t| t.slice_batch(start, start + chunk))
541 .collect();
542 for (i, output) in self.inner.run(part)?.into_iter().enumerate() {
543 if per_output.len() <= i {
544 per_output.push(Vec::new());
545 }
546 per_output[i].push(output);
547 }
548 }
549 per_output
550 .into_iter()
551 .map(HostTensor::concat_batch)
552 .collect()
553 }
554
555 fn check_outputs(&self, outputs: &[HostTensor], batch: usize) -> Result<()> {
556 if outputs.len() != self.outputs.len() {
557 return Err(ModelError::InferenceFailed(format!(
558 "model returned {} outputs, expected {}",
559 outputs.len(),
560 self.outputs.len()
561 ))
562 .into());
563 }
564 if let Some(output) = outputs.iter().find(|o| o.batch() != batch) {
565 return Err(ModelError::InferenceFailed(format!(
566 "model returned shape {:?} for a batch of {batch}",
567 output.shape
568 ))
569 .into());
570 }
571 Ok(())
572 }
573
574 pub fn predict(&self, input: Vec<f32>) -> Result<Vec<f32>> {
577 self.predict_batch(vec![input])
578 .map(|mut batch| batch.pop().expect("batch of one yields one output"))
579 }
580
581 pub fn predict_batch(&self, inputs: Vec<Vec<f32>>) -> Result<Vec<Vec<f32>>> {
586 let batch_size = inputs.len();
587 let flattened = match self.flatten_batch(inputs) {
588 Ok(flattened) => flattened,
589 Err(e) => {
590 self.stats
591 .lock()
592 .unwrap_or_else(|e| e.into_inner())
593 .record_error();
594 return Err(e);
595 }
596 };
597
598 let output_size: usize = self.info.output_spec.shape.iter().product();
599 Ok(self
600 .predict_flat(flattened, batch_size)?
601 .chunks_exact(output_size)
602 .map(<[f32]>::to_vec)
603 .collect())
604 }
605
606 pub fn predict_flat(&self, input: Vec<f32>, batch_size: usize) -> Result<Vec<f32>> {
612 self.predict_many(vec![FlatInput {
613 data: input,
614 batch_size,
615 }])
616 .pop()
617 .expect("one request yields one result")
618 }
619
620 pub fn predict_many(&self, requests: Vec<FlatInput>) -> Vec<Result<Vec<f32>>> {
628 let batches: Vec<usize> = requests.iter().map(|r| r.batch_size).collect();
629 let requests = requests.into_iter().map(|r| self.flat_request(r)).collect();
630 self.run_requests(requests, Door::Flat)
631 .into_iter()
632 .zip(batches)
633 .map(|(result, batch)| result.and_then(|outputs| self.flat_output(outputs, batch)))
634 .collect()
635 }
636
637 fn flat_request(&self, FlatInput { data, batch_size }: FlatInput) -> Result<Vec<HostTensor>> {
640 Self::check_batch_size(batch_size)?;
641 let item = &self.info.input_spec.shape;
642 if data.len() != batch_size * item.iter().product::<usize>() {
643 return Err(ModelError::InputValidation {
644 batch: batch_size,
645 item: item.clone(),
646 got: data.len(),
647 }
648 .into());
649 }
650 let mut shape = Vec::with_capacity(item.len() + 1);
651 shape.push(batch_size);
652 shape.extend_from_slice(item);
653 Ok(vec![HostTensor {
654 shape,
655 values: Values::F32(data),
656 }])
657 }
658
659 fn flat_output(&self, outputs: Vec<HostTensor>, batch_size: usize) -> Result<Vec<f32>> {
661 let output_size: usize = self.info.output_spec.shape.iter().product();
662 match outputs.into_iter().next() {
663 Some(HostTensor {
664 values: Values::F32(values),
665 ..
666 }) if values.len() == batch_size * output_size => Ok(values),
667 Some(output) => Err(ModelError::InferenceFailed(format!(
668 "model returned shape {:?}, expected [{batch_size}, ..{:?}]",
669 output.shape, self.info.output_spec.shape
670 ))
671 .into()),
672 None => Err(ModelError::InferenceFailed("model returned no output".to_string()).into()),
673 }
674 }
675
676 fn check_batch_size(batch_size: usize) -> Result<()> {
677 if batch_size == 0 {
678 return Err(ModelError::EmptyBatch.into());
679 }
680 if batch_size > MAX_BATCH_SIZE {
681 return Err(ModelError::BatchTooLarge {
682 size: batch_size,
683 max: MAX_BATCH_SIZE,
684 }
685 .into());
686 }
687 Ok(())
688 }
689
690 fn flatten_batch(&self, inputs: Vec<Vec<f32>>) -> Result<Vec<f32>> {
691 Self::check_batch_size(inputs.len())?;
692 let input_size: usize = self.info.input_spec.shape.iter().product();
693 let mut flattened = Vec::with_capacity(inputs.len() * input_size);
694 for input in inputs {
695 self.validate_input_shape(&input)?;
696 flattened.extend_from_slice(&input);
697 }
698 Ok(flattened)
699 }
700
701 pub fn adopt_stats(&mut self, previous: &Model) {
704 self.stats = previous.stats.clone();
705 }
706
707 pub fn warm_up(&self) -> Result<Option<std::time::Duration>> {
714 let start = Instant::now();
715 let batch = self.inner.fixed_batch().unwrap_or(1);
717 let Some(inputs) = self
718 .inputs
719 .iter()
720 .map(|spec| {
721 let mut shape = vec![batch];
722 shape.extend(spec.static_shape()?);
723 let len = shape.iter().product();
724 let values = if spec.dtype.is_int() {
725 Values::I64(vec![0; len])
726 } else {
727 Values::F32(vec![0.0; len])
728 };
729 Some(HostTensor { shape, values })
730 })
731 .collect::<Option<Vec<_>>>()
732 else {
733 return Ok(None);
734 };
735 self.inner.run(inputs)?;
736 Ok(Some(start.elapsed()))
737 }
738
739 pub fn get_info(&self) -> &ModelInfo {
741 &self.info
742 }
743
744 pub fn get_stats(&self) -> ModelStats {
746 self.stats.lock().unwrap_or_else(|e| e.into_inner()).clone()
747 }
748
749 pub fn validate_input_shape(&self, input: &[f32]) -> Result<()> {
751 let expected_size: usize = self.info.input_spec.shape.iter().product();
752 if input.len() != expected_size {
753 return Err(ModelError::InputValidation {
754 batch: 1,
755 item: self.info.input_spec.shape.clone(),
756 got: input.len(),
757 }
758 .into());
759 }
760 Ok(())
761 }
762
763 pub fn get_backend_info(&self) -> String {
765 self.info.backend.clone()
766 }
767
768 pub fn model_type(&self) -> &'static str {
770 self.model_type
771 }
772}
773
774fn join_requests(requests: Vec<Vec<HostTensor>>) -> Result<Vec<HostTensor>> {
776 let count = requests.first().map_or(0, Vec::len);
777 let mut columns: Vec<Vec<HostTensor>> = (0..count).map(|_| Vec::new()).collect();
778 for inputs in requests {
779 for (column, tensor) in columns.iter_mut().zip(inputs) {
780 column.push(tensor);
781 }
782 }
783 columns.into_iter().map(HostTensor::concat_batch).collect()
784}
785
786fn duplicate(error: &FurnaceError) -> FurnaceError {
789 let FurnaceError::Model(error) = error else {
790 return ModelError::InferenceFailed(error.to_string()).into();
791 };
792 match error {
793 ModelError::LoadFailed { path, source } => ModelError::LoadFailed {
794 path: path.clone(),
795 source: source.to_string().into(),
796 },
797 ModelError::InvalidFormat(m) => ModelError::InvalidFormat(m.clone()),
798 ModelError::FileNotFound(path) => ModelError::FileNotFound(path.clone()),
799 ModelError::InferenceFailed(m) => ModelError::InferenceFailed(m.clone()),
800 ModelError::InputValidation { batch, item, got } => ModelError::InputValidation {
801 batch: *batch,
802 item: item.clone(),
803 got: *got,
804 },
805 ModelError::EmptyBatch => ModelError::EmptyBatch,
806 ModelError::BatchTooLarge { size, max } => ModelError::BatchTooLarge {
807 size: *size,
808 max: *max,
809 },
810 ModelError::InvalidDataType(m) => ModelError::InvalidDataType(m.clone()),
811 ModelError::InvalidInput(m) => ModelError::InvalidInput(m.clone()),
812 ModelError::InvalidArgument { arg, value, reason } => ModelError::InvalidArgument {
813 arg: arg.clone(),
814 value: value.clone(),
815 reason: reason.clone(),
816 },
817 }
818 .into()
819}
820
821#[cfg(test)]
822mod tests {
823 use super::*;
824
825 fn get_test_model() -> Model {
826 let dummy = DummyModel::new("test".to_string(), vec![784], vec![10]);
827 Model::new(Box::new(dummy), "dummy", 0)
828 }
829
830 fn tensors(ids: Vec<i64>, scale: Vec<f32>) -> Vec<HostTensor> {
831 vec![
832 HostTensor {
833 shape: vec![scale.len(), 4],
834 values: Values::I64(ids),
835 },
836 HostTensor {
837 shape: vec![scale.len()],
838 values: Values::F32(scale),
839 },
840 ]
841 }
842
843 #[test]
844 fn named_tensors_in_and_out() {
845 let model = Model::new(Box::new(DummyTensorsModel::new(None)), "dummy", 0);
846 assert!(!model.is_simple());
847 let info = model.get_info();
848 assert_eq!(info.inputs[0].datatype, "INT64");
849 assert_eq!(info.inputs[0].shape, [-1, 4]);
850 assert_eq!(info.inputs[1].shape, [-1]);
851 assert_eq!(info.input_spec.dtype, "int64");
852
853 let outputs = model
854 .infer(tensors(vec![1, 2, 3, 4, 5, 6, 7, 8], vec![0.5, 2.0]))
855 .unwrap();
856 assert_eq!(outputs[0].shape, [2, 4]);
857 assert_eq!(
858 outputs[0].values,
859 Values::F32(vec![0.5, 1.0, 1.5, 2.0, 10.0, 12.0, 14.0, 16.0])
860 );
861 assert_eq!(outputs[1].values, Values::I64(vec![10, 26]));
862 assert_eq!(model.get_stats().success_count, 1);
863 assert!(model.warm_up().unwrap().is_some());
865 }
866
867 #[test]
868 fn named_tensors_are_checked() {
869 let model = Model::new(Box::new(DummyTensorsModel::new(None)), "dummy", 0);
870 let check = |inputs: Vec<HostTensor>, expected: &str| {
871 let err = model.infer(inputs).unwrap_err().to_string();
872 assert!(err.contains(expected), "{expected:?} not in {err:?}");
873 };
874 let mut inputs = tensors(vec![1, 2, 3, 4], vec![1.0]);
875 inputs.pop();
876 check(inputs, "expected 2 inputs (ids, scale)");
877 let mut swapped = tensors(vec![1, 2, 3, 4], vec![1.0]);
878 swapped.swap(0, 1);
879 check(swapped, "'ids' must be INT64");
880 let mut short = tensors(vec![1, 2, 3], vec![1.0]);
881 short[0].shape = vec![1, 3];
882 check(short, "'ids' has shape [1, 3]");
883 check(
884 tensors(vec![1, 2, 3, 4, 5, 6, 7, 8], vec![1.0]),
885 "'ids' has shape [1, 4] (4 values) but 8 values",
886 );
887 let mut disagree = tensors(vec![1, 2, 3, 4, 5, 6, 7, 8], vec![1.0, 2.0]);
888 disagree[1] = HostTensor {
889 shape: vec![1],
890 values: Values::F32(vec![1.0]),
891 };
892 check(disagree, "disagree on the batch size");
893 check(
894 tensors(vec![1, 2, 3, 4], vec![f32::NAN]),
895 "'scale' has a non-finite value",
896 );
897 assert_eq!(model.get_stats().error_count, 6);
898 }
899
900 #[test]
901 fn requests_of_named_tensors_share_a_forward_pass() {
902 let model = Model::new(Box::new(DummyTensorsModel::new(None)), "dummy", 0);
903 assert!(model.batches_named());
904 let mut bad_shape = tensors(vec![1, 2, 3, 4], vec![1.0]);
905 bad_shape[0].shape = vec![2, 2];
906 let results = model.infer_many(vec![
907 tensors(vec![1, 2, 3, 4], vec![1.0]),
908 bad_shape,
909 tensors(vec![1, 1, 1, 1, 2, 2, 2, 2], vec![2.0, 0.5]),
910 tensors(vec![i64::from(i32::MAX) + 1, 0, 0, 0], vec![1.0]),
911 ]);
912 let first = results[0].as_ref().unwrap();
914 assert_eq!(first[0].shape, [1, 4]);
915 assert_eq!(first[1].values, Values::I64(vec![10]));
916 let third = results[2].as_ref().unwrap();
917 assert_eq!(third[0].shape, [2, 4]);
918 assert_eq!(
919 third[0].values,
920 Values::F32(vec![2.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0])
921 );
922 assert_eq!(third[1].values, Values::I64(vec![4, 8]));
923 assert!(
925 results[1]
926 .as_ref()
927 .unwrap_err()
928 .to_string()
929 .contains("'ids'")
930 );
931 let range = results[3].as_ref().unwrap_err().to_string();
932 assert!(range.contains("outside"), "{range}");
933 let stats = model.get_stats();
934 assert_eq!((stats.batch_count, stats.batched_items), (1, 3));
935 assert_eq!((stats.success_count, stats.error_count), (2, 2));
936 }
937
938 #[test]
939 fn fixed_batch_graphs_run_in_chunks() {
940 let model = Model::new(Box::new(DummyTensorsModel::new(Some(2))), "dummy", 0);
941 let outputs = model
942 .infer(tensors((1..=24).collect(), vec![1.0; 6]))
943 .unwrap();
944 assert_eq!(outputs[0].shape, [6, 4]);
945 assert_eq!(outputs[1].values, Values::I64(vec![10, 26, 42, 58, 74, 90]));
946 let err = model
947 .infer(tensors((1..=12).collect(), vec![1.0; 3]))
948 .unwrap_err()
949 .to_string();
950 assert!(err.contains("runs batches of 2"), "{err}");
951 assert!(model.warm_up().unwrap().is_some());
953 }
954
955 #[derive(Debug)]
957 struct PairsOnly(DummyModel);
958
959 impl TensorModel for PairsOnly {
960 fn name(&self) -> &str {
961 self.0.name()
962 }
963 fn backend(&self) -> &str {
964 self.0.backend()
965 }
966 fn inputs(&self) -> Vec<IoSpec> {
967 self.0.inputs()
968 }
969 fn outputs(&self) -> Vec<IoSpec> {
970 self.0.outputs()
971 }
972 fn run(&self, inputs: Vec<HostTensor>) -> Result<Vec<HostTensor>> {
973 self.0.run(inputs)
974 }
975 fn fixed_batch(&self) -> Option<usize> {
976 Some(2)
977 }
978 }
979
980 #[test]
981 fn a_batch_the_graph_cannot_run_is_the_clients_error() {
982 let dummy = DummyModel::new("pairs".to_string(), vec![3], vec![2]);
983 let model = Model::new(Box::new(PairsOnly(dummy)), "dummy", 0);
984 let err = model
985 .predict_many(vec![FlatInput {
986 data: vec![1.0; 9],
987 batch_size: 3,
988 }])
989 .pop()
990 .unwrap()
991 .unwrap_err();
992 assert!(err.is_client_error(), "{err:?}");
993 assert!(!err.to_string().contains("Inference failed"), "{err}");
994 assert!(err.to_string().contains("runs batches of 2"), "{err}");
995 let pairs = model.predict_many(vec![FlatInput {
996 data: vec![1.0; 12],
997 batch_size: 4,
998 }]);
999 assert_eq!(pairs[0].as_ref().unwrap().len(), 8);
1000 assert!(model.warm_up().unwrap().is_some());
1001 }
1002
1003 #[test]
1004 fn a_shared_failure_keeps_its_error_for_every_request() {
1005 let model = Model::new(Box::new(DummyTensorsModel::new(Some(2))), "dummy", 0);
1008 let results = model.infer_many(vec![
1009 tensors(vec![1, 2, 3, 4], vec![1.0]),
1010 tensors(vec![1, 2, 3, 4, 5, 6, 7, 8], vec![1.0, 2.0]),
1011 ]);
1012 for result in results {
1013 let err = result.unwrap_err();
1014 assert!(
1015 matches!(
1016 err,
1017 crate::error::FurnaceError::Model(ModelError::InvalidInput(_))
1018 ),
1019 "{err:?}"
1020 );
1021 assert!(err.to_string().contains("runs batches of 2"), "{err}");
1022 }
1023 assert_eq!(model.get_stats().error_count, 2);
1024 }
1025
1026 #[test]
1027 fn an_input_without_a_batch_dimension_is_rejected() {
1028 let model = Model::new(Box::new(DummyTensorsModel::new(None)), "dummy", 0);
1029 let mut inputs = tensors(vec![1, 2, 3, 4], vec![1.0]);
1030 inputs[1].shape = vec![];
1031 assert_eq!(inputs[1].batch(), 0);
1032 let err = model.infer(inputs).unwrap_err().to_string();
1033 assert!(err.contains("'scale' has no batch dimension"), "{err}");
1034 }
1035
1036 #[test]
1037 fn test_model_info() {
1038 let model = get_test_model();
1039 let info = model.get_info();
1040 assert_eq!(info.model_type, "dummy");
1041 assert_eq!(info.backend, "cpu");
1042 assert_eq!(info.input_spec.shape, vec![784]);
1043 assert_eq!(info.output_spec.shape, vec![10]);
1044 assert_eq!(info.input_spec.dtype, "float32");
1045 }
1046
1047 #[test]
1048 fn test_inference_with_valid_input() {
1049 let model = get_test_model();
1050 let output = model.predict(vec![0.5; 784]).unwrap();
1051 assert_eq!(output.len(), 10);
1052 }
1053
1054 #[test]
1055 fn test_batch_inference_splits_outputs() {
1056 let model = get_test_model();
1057 let outputs = model.predict_batch(vec![vec![0.5; 784]; 3]).unwrap();
1058 assert_eq!(outputs.len(), 3);
1059 assert!(outputs.iter().all(|o| o.len() == 10));
1060 }
1061
1062 #[test]
1063 fn test_predict_flat_matches_batch() {
1064 let model = get_test_model();
1065 let flat = model.predict_flat(vec![0.5; 784 * 3], 3).unwrap();
1066 let batch = model.predict_batch(vec![vec![0.5; 784]; 3]).unwrap();
1067 assert_eq!(flat, batch.concat());
1068 assert_eq!(model.get_stats().success_count, 2);
1069 }
1070
1071 #[test]
1072 fn test_predict_flat_rejects_bad_input() {
1073 let model = get_test_model();
1074 let err = model.predict_flat(vec![0.5; 784 * 2 + 1], 2).unwrap_err();
1076 assert!(matches!(
1077 err,
1078 crate::error::FurnaceError::Model(ModelError::InputValidation { .. })
1079 ));
1080 assert_eq!(
1081 err.to_string(),
1082 "expected 1568 values (batch 2 × [784]), got 1569"
1083 );
1084 let err = model.predict_flat(vec![], 0).unwrap_err();
1085 assert!(matches!(
1086 err,
1087 crate::error::FurnaceError::Model(ModelError::EmptyBatch)
1088 ));
1089 let err = model
1090 .predict_flat(vec![0.5; 784], MAX_BATCH_SIZE + 1)
1091 .unwrap_err();
1092 assert!(matches!(
1093 err,
1094 crate::error::FurnaceError::Model(ModelError::BatchTooLarge { .. })
1095 ));
1096
1097 let mut input = vec![0.5; 784 * 2];
1099 input[784 + 5] = f32::NAN;
1100 let err = model.predict_flat(input, 2).unwrap_err().to_string();
1101 assert!(err.contains("batch[1][5]"), "{err}");
1102 assert_eq!(model.get_stats().error_count, 4);
1103 }
1104
1105 #[test]
1106 fn test_inference_with_invalid_shape() {
1107 let model = get_test_model();
1108 let result = model.predict(vec![0.5; 100]);
1109 assert!(matches!(
1110 result,
1111 Err(crate::error::FurnaceError::Model(
1112 ModelError::InputValidation { .. }
1113 ))
1114 ));
1115 }
1116
1117 #[test]
1118 fn test_inference_rejects_non_finite() {
1119 let model = get_test_model();
1120 let mut input = vec![0.5; 784];
1121 input[42] = f32::NAN;
1122 assert!(matches!(
1123 model.predict(input),
1124 Err(crate::error::FurnaceError::Model(
1125 ModelError::InvalidDataType(_)
1126 ))
1127 ));
1128 }
1129
1130 #[test]
1131 fn a_reloaded_model_shares_its_predecessors_stats() {
1132 let old = get_test_model();
1133 old.predict(vec![0.5; 784]).unwrap();
1134 let mut new = get_test_model();
1135 new.adopt_stats(&old);
1136 old.predict(vec![0.5; 784]).unwrap();
1138 new.predict(vec![0.5; 784]).unwrap();
1139 assert_eq!(new.get_stats().success_count, 3);
1140 assert_eq!(old.get_stats().success_count, 3);
1141 }
1142
1143 #[test]
1144 fn test_model_stats() {
1145 let model = get_test_model();
1146 let stats = model.get_stats();
1147 assert_eq!(stats.inference_count, 0);
1148 assert!(stats.last_inference_time.is_none());
1149
1150 model.predict(vec![0.5; 784]).unwrap();
1151 let _ = model.predict(vec![0.5; 1]);
1152
1153 let stats = model.get_stats();
1154 assert_eq!(stats.inference_count, 2);
1155 assert_eq!(stats.success_count, 1);
1156 assert_eq!(stats.error_count, 1);
1157 assert!(stats.last_inference_time.is_some());
1158 assert!(stats.min_inference_time_ms.is_some());
1159 }
1160}