1use std::collections::HashMap;
75use std::fmt::Debug;
76
77use scirs2_core::error::ErrorContext;
78use scirs2_core::ndarray::{Array2, ArrayD, IxDyn};
79use scirs2_core::numeric::Float;
80
81use super::frontend::{
82 ConstantValue, OperandId, OperationId, OperationType, ReduceOperation, ReductionFunction,
83 TensorShape, XLAComputation, XLAOperation,
84};
85use crate::error::{OptimError, Result};
86
87pub type ValueMap = HashMap<OperandId, ArrayD<f64>>;
92
93#[derive(Debug, Default, Clone)]
95pub struct ReferenceExecutor {
96 evaluated_operations: usize,
98}
99
100impl ReferenceExecutor {
101 pub fn new() -> Self {
103 Self::default()
104 }
105
106 pub fn evaluated_operations(&self) -> usize {
108 self.evaluated_operations
109 }
110
111 pub fn execute<T>(
117 &mut self,
118 computation: &XLAComputation<T>,
119 inputs: ValueMap,
120 ) -> Result<Vec<ArrayD<f64>>>
121 where
122 T: Float + Debug + Default + Clone + Send + Sync + 'static,
123 {
124 let mut values = self.evaluate_all(computation, inputs)?;
125
126 if computation.outputs.is_empty() {
127 return Err(OptimError::ValidationError(ErrorContext::new(format!(
128 "computation '{}' declares no outputs; nothing to return",
129 computation.metadata.name
130 ))));
131 }
132
133 let mut results = Vec::with_capacity(computation.outputs.len());
134 for output in &computation.outputs {
135 let value = values.remove(&output.operand).ok_or_else(|| {
136 OptimError::ComputationError(ErrorContext::new(format!(
137 "output {} of computation '{}' references operand {:?}, which was never \
138 produced",
139 output.index, computation.metadata.name, output.operand
140 )))
141 })?;
142 results.push(value);
143 }
144
145 Ok(results)
146 }
147
148 pub fn evaluate_all<T>(
152 &mut self,
153 computation: &XLAComputation<T>,
154 inputs: ValueMap,
155 ) -> Result<ValueMap>
156 where
157 T: Float + Debug + Default + Clone + Send + Sync + 'static,
158 {
159 self.evaluated_operations = 0;
160 let mut values: ValueMap = inputs;
161
162 for operation in Self::execution_order(computation)? {
163 if values.contains_key(&operation.output) {
167 continue;
168 }
169 let result = self.evaluate_operation(computation, operation, &values)?;
170 values.insert(operation.output, result);
171 self.evaluated_operations += 1;
172 }
173
174 Ok(values)
175 }
176
177 fn execution_order<T>(computation: &XLAComputation<T>) -> Result<Vec<&XLAOperation<T>>>
179 where
180 T: Float + Debug + Default + Clone + Send + Sync + 'static,
181 {
182 let by_id: HashMap<OperationId, &XLAOperation<T>> = computation
183 .operations
184 .iter()
185 .map(|op| (op.id, op))
186 .collect();
187
188 let producers: HashMap<OperandId, OperationId> = computation
191 .operations
192 .iter()
193 .map(|op| (op.output, op.id))
194 .collect();
195
196 let mut in_degree: HashMap<OperationId, usize> = HashMap::new();
197 let mut dependents: HashMap<OperationId, Vec<OperationId>> = HashMap::new();
198
199 for operation in &computation.operations {
200 in_degree.entry(operation.id).or_insert(0);
201 for input in &operation.inputs {
202 if let Some(&producer) = producers.get(input) {
203 dependents.entry(producer).or_default().push(operation.id);
204 *in_degree.entry(operation.id).or_insert(0) += 1;
205 }
206 }
207 }
208
209 let mut ready: Vec<OperationId> = computation
210 .operations
211 .iter()
212 .filter(|op| in_degree.get(&op.id) == Some(&0))
213 .map(|op| op.id)
214 .collect();
215
216 let mut order: Vec<&XLAOperation<T>> = Vec::with_capacity(computation.operations.len());
217 while let Some(op_id) = ready.pop() {
218 if let Some(&operation) = by_id.get(&op_id) {
219 order.push(operation);
220 }
221 for next in dependents.get(&op_id).cloned().unwrap_or_default() {
222 if let Some(degree) = in_degree.get_mut(&next) {
223 *degree = degree.saturating_sub(1);
224 if *degree == 0 {
225 ready.push(next);
226 }
227 }
228 }
229 }
230
231 if order.len() != computation.operations.len() {
232 return Err(OptimError::ValidationError(ErrorContext::new(format!(
233 "computation '{}' contains a dependency cycle and cannot be executed",
234 computation.metadata.name
235 ))));
236 }
237
238 Ok(order)
239 }
240
241 fn evaluate_operation<T>(
243 &self,
244 computation: &XLAComputation<T>,
245 operation: &XLAOperation<T>,
246 values: &ValueMap,
247 ) -> Result<ArrayD<f64>>
248 where
249 T: Float + Debug + Default + Clone + Send + Sync + 'static,
250 {
251 let output_shape = computation
252 .operands
253 .get(&operation.output)
254 .map(|operand| operand.shape.clone())
255 .unwrap_or_default();
256
257 let mut operands: Vec<&ArrayD<f64>> = Vec::with_capacity(operation.inputs.len());
259 for input in &operation.inputs {
260 let value = values.get(input).ok_or_else(|| {
261 OptimError::ComputationError(ErrorContext::new(format!(
262 "operation {:?} ({:?}) reads operand {:?} before it is defined",
263 operation.id, operation.op_type, input
264 )))
265 })?;
266 operands.push(value);
267 }
268
269 match (&operation.op_type, operands.as_slice()) {
270 (OperationType::Constant(value), _) => Ok(constant_to_array(value)),
271
272 (OperationType::Parameter, _) => {
273 Err(OptimError::InvalidInput(ErrorContext::new(format!(
274 "no input value was supplied for parameter operand {:?} of computation '{}'",
275 operation.output, computation.metadata.name
276 ))))
277 }
278
279 (OperationType::Add, [a, b]) => binary(a, b, |x, y| x + y),
281 (OperationType::Subtract, [a, b]) => binary(a, b, |x, y| x - y),
282 (OperationType::Multiply, [a, b]) => binary(a, b, |x, y| x * y),
283 (OperationType::Divide, [a, b]) => binary(a, b, |x, y| x / y),
284 (OperationType::Maximum, [a, b]) => binary(a, b, f64::max),
285 (OperationType::Minimum, [a, b]) => binary(a, b, f64::min),
286 (OperationType::And, [a, b]) => binary(a, b, |x, y| bool_to_f64(x != 0.0 && y != 0.0)),
287 (OperationType::Or, [a, b]) => binary(a, b, |x, y| bool_to_f64(x != 0.0 || y != 0.0)),
288 (OperationType::Xor, [a, b]) => {
289 binary(a, b, |x, y| bool_to_f64((x != 0.0) != (y != 0.0)))
290 }
291 (OperationType::Equal, [a, b]) => binary(a, b, |x, y| bool_to_f64(x == y)),
292 (OperationType::NotEqual, [a, b]) => binary(a, b, |x, y| bool_to_f64(x != y)),
293 (OperationType::Less, [a, b]) => binary(a, b, |x, y| bool_to_f64(x < y)),
294 (OperationType::LessEqual, [a, b]) => binary(a, b, |x, y| bool_to_f64(x <= y)),
295 (OperationType::Greater, [a, b]) => binary(a, b, |x, y| bool_to_f64(x > y)),
296 (OperationType::GreaterEqual, [a, b]) => binary(a, b, |x, y| bool_to_f64(x >= y)),
297
298 (OperationType::Negate, [a]) => Ok(a.mapv(|x| -x)),
300 (OperationType::Abs, [a]) => Ok(a.mapv(f64::abs)),
301 (OperationType::Square, [a]) => Ok(a.mapv(|x| x * x)),
302 (OperationType::Sqrt, [a]) => Ok(a.mapv(f64::sqrt)),
303 (OperationType::Rsqrt, [a]) => Ok(a.mapv(|x| 1.0 / x.sqrt())),
304 (OperationType::Exp, [a]) => Ok(a.mapv(f64::exp)),
305 (OperationType::Log, [a]) => Ok(a.mapv(f64::ln)),
306 (OperationType::Sin, [a]) => Ok(a.mapv(f64::sin)),
307 (OperationType::Cos, [a]) => Ok(a.mapv(f64::cos)),
308 (OperationType::Tanh, [a]) => Ok(a.mapv(f64::tanh)),
309 (OperationType::Ceil, [a]) => Ok(a.mapv(f64::ceil)),
310 (OperationType::Floor, [a]) => Ok(a.mapv(f64::floor)),
311 (OperationType::Round, [a]) => Ok(a.mapv(f64::round)),
312 (OperationType::Sign, [a]) => Ok(a.mapv(|x| {
313 if x > 0.0 {
314 1.0
315 } else if x < 0.0 {
316 -1.0
317 } else {
318 0.0
319 }
320 })),
321 (OperationType::Not, [a]) => Ok(a.mapv(|x| bool_to_f64(x == 0.0))),
322 (OperationType::Copy, [a]) => Ok((*a).clone()),
323
324 (OperationType::Dot, [a, b])
326 | (OperationType::DotGeneral, [a, b])
327 | (OperationType::MatMul, [a, b]) => matmul(a, b),
328
329 (OperationType::Reshape, [a]) => reshape(a, &output_shape),
331 (OperationType::Transpose, [a]) => Ok(a.t().to_owned()),
332 (OperationType::Broadcast, [a]) => broadcast_to(a, &output_shape),
333
334 (OperationType::Reduce(reduce_op), [a]) => reduce(a, reduce_op),
335
336 (OperationType::Concatenate, values) if !values.is_empty() => {
337 concatenate(values, &operation.attributes)
338 }
339
340 (OperationType::Tuple, _) | (OperationType::GetTupleElement, _) => {
342 Err(OptimError::NotImplementedError(ErrorContext::new(format!(
343 "the CPU reference executor does not model tuple values, so {:?} \
344 (operation {:?}) cannot be evaluated",
345 operation.op_type, operation.id
346 ))))
347 }
348
349 (op_type, operands) => {
351 Err(OptimError::NotImplementedError(ErrorContext::new(format!(
352 "the CPU reference executor does not implement {:?} with {} operand(s) \
353 (operation {:?} in computation '{}')",
354 op_type,
355 operands.len(),
356 operation.id,
357 computation.metadata.name
358 ))))
359 }
360 }
361 }
362}
363
364fn constant_to_array(value: &ConstantValue) -> ArrayD<f64> {
366 let shape = IxDyn(&value.dims);
367 ArrayD::from_shape_vec(shape, value.data.clone()).unwrap_or_else(|_| {
368 ArrayD::from_shape_vec(IxDyn(&[value.data.len()]), value.data.clone())
371 .unwrap_or_else(|_| ArrayD::zeros(IxDyn(&[0])))
372 })
373}
374
375fn bool_to_f64(value: bool) -> f64 {
376 if value {
377 1.0
378 } else {
379 0.0
380 }
381}
382
383fn binary(
385 lhs: &ArrayD<f64>,
386 rhs: &ArrayD<f64>,
387 op: impl Fn(f64, f64) -> f64,
388) -> Result<ArrayD<f64>> {
389 if lhs.shape() == rhs.shape() {
390 let mut out = lhs.clone();
391 for (slot, &value) in out.iter_mut().zip(rhs.iter()) {
392 *slot = op(*slot, value);
393 }
394 return Ok(out);
395 }
396
397 if lhs.len() == 1 {
398 let scalar = lhs.iter().next().copied().unwrap_or(0.0);
399 return Ok(rhs.mapv(|value| op(scalar, value)));
400 }
401
402 if rhs.len() == 1 {
403 let scalar = rhs.iter().next().copied().unwrap_or(0.0);
404 return Ok(lhs.mapv(|value| op(value, scalar)));
405 }
406
407 Err(OptimError::ShapeError(ErrorContext::new(format!(
408 "cannot apply an element-wise operation to shapes {:?} and {:?}; only rank-0 \
409 broadcasting is modelled",
410 lhs.shape(),
411 rhs.shape()
412 ))))
413}
414
415fn matmul(lhs: &ArrayD<f64>, rhs: &ArrayD<f64>) -> Result<ArrayD<f64>> {
417 let lhs_2d: Array2<f64> = lhs.clone().into_dimensionality().map_err(|_| {
418 OptimError::ShapeError(ErrorContext::new(format!(
419 "matrix product requires a rank-2 left operand, got shape {:?}",
420 lhs.shape()
421 )))
422 })?;
423 let rhs_2d: Array2<f64> = rhs.clone().into_dimensionality().map_err(|_| {
424 OptimError::ShapeError(ErrorContext::new(format!(
425 "matrix product requires a rank-2 right operand, got shape {:?}",
426 rhs.shape()
427 )))
428 })?;
429
430 if lhs_2d.ncols() != rhs_2d.nrows() {
431 return Err(OptimError::ShapeError(ErrorContext::new(format!(
432 "matrix product contraction mismatch: {:?} x {:?}",
433 lhs_2d.shape(),
434 rhs_2d.shape()
435 ))));
436 }
437
438 Ok(lhs_2d.dot(&rhs_2d).into_dyn())
439}
440
441fn reshape(value: &ArrayD<f64>, target: &TensorShape) -> Result<ArrayD<f64>> {
443 let expected: usize = if target.dimensions.is_empty() {
444 1
445 } else {
446 target.dimensions.iter().product()
447 };
448
449 if expected != value.len() {
450 return Err(OptimError::ShapeError(ErrorContext::new(format!(
451 "reshape changes the element count: {} elements cannot become shape {:?}",
452 value.len(),
453 target.dimensions
454 ))));
455 }
456
457 let flat: Vec<f64> = value.iter().copied().collect();
458 ArrayD::from_shape_vec(IxDyn(&target.dimensions), flat).map_err(|error| {
459 OptimError::ShapeError(ErrorContext::new(format!("reshape failed: {error}")))
460 })
461}
462
463fn broadcast_to(value: &ArrayD<f64>, target: &TensorShape) -> Result<ArrayD<f64>> {
465 let count: usize = if target.dimensions.is_empty() {
466 1
467 } else {
468 target.dimensions.iter().product()
469 };
470
471 if value.len() == count {
472 return reshape(value, target);
473 }
474
475 if value.len() == 1 {
476 let scalar = value.iter().next().copied().unwrap_or(0.0);
477 return Ok(ArrayD::from_elem(IxDyn(&target.dimensions), scalar));
478 }
479
480 Err(OptimError::NotImplementedError(ErrorContext::new(format!(
481 "the CPU reference executor only broadcasts rank-0 scalars; cannot broadcast shape \
482 {:?} to {:?}",
483 value.shape(),
484 target.dimensions
485 ))))
486}
487
488fn reduce(value: &ArrayD<f64>, reduce_op: &ReduceOperation) -> Result<ArrayD<f64>> {
490 let (init, combine): (f64, fn(f64, f64) -> f64) = match reduce_op.function {
491 ReductionFunction::Add => (0.0, |a, b| a + b),
492 ReductionFunction::Multiply => (1.0, |a, b| a * b),
493 ReductionFunction::Max => (f64::NEG_INFINITY, f64::max),
494 ReductionFunction::Min => (f64::INFINITY, f64::min),
495 ReductionFunction::And => (1.0, |a, b| bool_to_f64(a != 0.0 && b != 0.0)),
496 ReductionFunction::Or => (0.0, |a, b| bool_to_f64(a != 0.0 || b != 0.0)),
497 ReductionFunction::Xor => (0.0, |a, b| bool_to_f64((a != 0.0) != (b != 0.0))),
498 };
499
500 let reduces_all = reduce_op.dimensions.is_empty() || reduce_op.dimensions.len() == value.ndim();
502
503 if reduces_all {
504 let total = value.iter().copied().fold(init, combine);
505 return ArrayD::from_shape_vec(IxDyn(&[]), vec![total]).map_err(|error| {
506 OptimError::ShapeError(ErrorContext::new(format!("reduce failed: {error}")))
507 });
508 }
509
510 let mut current = value.clone();
511 let mut axes = reduce_op.dimensions.clone();
513 axes.sort_unstable();
514 axes.dedup();
515
516 for &axis in axes.iter().rev() {
517 if axis >= current.ndim() {
518 return Err(OptimError::ShapeError(ErrorContext::new(format!(
519 "reduce dimension {axis} is out of range for a rank-{} operand",
520 current.ndim()
521 ))));
522 }
523 current = current.fold_axis(scirs2_core::ndarray::Axis(axis), init, |&a, &b| {
524 combine(a, b)
525 });
526 }
527
528 Ok(current)
529}
530
531fn concatenate(
533 values: &[&ArrayD<f64>],
534 attributes: &super::frontend::OperationAttributes,
535) -> Result<ArrayD<f64>> {
536 use super::frontend::AttributeValue;
537
538 let axis = match attributes.attributes.get("dimension") {
539 Some(AttributeValue::Int(value)) if *value >= 0 => *value as usize,
540 Some(other) => {
541 return Err(OptimError::InvalidInput(ErrorContext::new(format!(
542 "concatenate `dimension` attribute must be a non-negative Int, got {other:?}"
543 ))))
544 }
545 None => 0,
546 };
547
548 let views: Vec<_> = values.iter().map(|value| value.view()).collect();
549 scirs2_core::ndarray::concatenate(scirs2_core::ndarray::Axis(axis), &views).map_err(|error| {
550 OptimError::ShapeError(ErrorContext::new(format!("concatenate failed: {error}")))
551 })
552}
553
554#[cfg(test)]
555mod tests {
556 use super::super::frontend::graph_capture::test_support::{add_op, scalar_shape, shape};
557 use super::super::frontend::ComputationGraphBuilder;
558 use super::*;
559
560 #[test]
562 fn executes_a_three_operation_graph() {
563 let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
564 let mut comp = builder.create_computation("add");
565
566 let two = add_op(
567 &mut builder,
568 &mut comp,
569 OperationType::Constant(ConstantValue::scalar(2.0)),
570 vec![],
571 scalar_shape(),
572 );
573 let three = add_op(
574 &mut builder,
575 &mut comp,
576 OperationType::Constant(ConstantValue::scalar(3.0)),
577 vec![],
578 scalar_shape(),
579 );
580 let _sum = add_op(
581 &mut builder,
582 &mut comp,
583 OperationType::Add,
584 vec![two, three],
585 scalar_shape(),
586 );
587 builder
588 .mark_terminal_operands_as_outputs(&mut comp)
589 .expect("outputs must be inferable");
590
591 let outputs = ReferenceExecutor::new()
592 .execute(&comp, ValueMap::new())
593 .expect("execution must succeed");
594
595 assert_eq!(outputs.len(), 1);
596 assert_eq!(outputs[0].iter().copied().collect::<Vec<_>>(), vec![5.0]);
597 }
598
599 #[test]
601 fn executes_with_parameter_inputs() {
602 let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
603 let mut comp = builder.create_computation("scale");
604
605 let x = add_op(
606 &mut builder,
607 &mut comp,
608 OperationType::Parameter,
609 vec![],
610 shape(&[3]),
611 );
612 let factor = add_op(
613 &mut builder,
614 &mut comp,
615 OperationType::Constant(ConstantValue::scalar(10.0)),
616 vec![],
617 scalar_shape(),
618 );
619 let _scaled = add_op(
620 &mut builder,
621 &mut comp,
622 OperationType::Multiply,
623 vec![x, factor],
624 shape(&[3]),
625 );
626 builder
627 .mark_terminal_operands_as_outputs(&mut comp)
628 .expect("outputs must be inferable");
629
630 let mut inputs = ValueMap::new();
631 inputs.insert(
632 x,
633 ArrayD::from_shape_vec(IxDyn(&[3]), vec![1.0, 2.0, 3.0])
634 .expect("input array must build"),
635 );
636
637 let outputs = ReferenceExecutor::new()
638 .execute(&comp, inputs)
639 .expect("execution must succeed");
640
641 assert_eq!(
642 outputs[0].iter().copied().collect::<Vec<_>>(),
643 vec![10.0, 20.0, 30.0]
644 );
645 }
646
647 #[test]
649 fn missing_parameter_input_is_an_error() {
650 let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
651 let mut comp = builder.create_computation("unbound");
652
653 let x = add_op(
654 &mut builder,
655 &mut comp,
656 OperationType::Parameter,
657 vec![],
658 shape(&[2]),
659 );
660 let _out = add_op(
661 &mut builder,
662 &mut comp,
663 OperationType::Abs,
664 vec![x],
665 shape(&[2]),
666 );
667 builder
668 .mark_terminal_operands_as_outputs(&mut comp)
669 .expect("outputs must be inferable");
670
671 assert!(ReferenceExecutor::new()
672 .execute(&comp, ValueMap::new())
673 .is_err());
674 }
675
676 #[test]
678 fn executes_matrix_multiplication() {
679 let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
680 let mut comp = builder.create_computation("matmul");
681
682 let a = add_op(
683 &mut builder,
684 &mut comp,
685 OperationType::Constant(
686 ConstantValue::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2])
687 .expect("constant must be well formed"),
688 ),
689 vec![],
690 shape(&[2, 2]),
691 );
692 let b = add_op(
693 &mut builder,
694 &mut comp,
695 OperationType::Constant(
696 ConstantValue::new(vec![5.0, 6.0, 7.0, 8.0], vec![2, 2])
697 .expect("constant must be well formed"),
698 ),
699 vec![],
700 shape(&[2, 2]),
701 );
702 let _product = add_op(
703 &mut builder,
704 &mut comp,
705 OperationType::MatMul,
706 vec![a, b],
707 shape(&[2, 2]),
708 );
709 builder
710 .mark_terminal_operands_as_outputs(&mut comp)
711 .expect("outputs must be inferable");
712
713 let outputs = ReferenceExecutor::new()
714 .execute(&comp, ValueMap::new())
715 .expect("execution must succeed");
716
717 assert_eq!(
719 outputs[0].iter().copied().collect::<Vec<_>>(),
720 vec![19.0, 22.0, 43.0, 50.0]
721 );
722 }
723
724 #[test]
726 fn executes_reshape() {
727 let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
728 let mut comp = builder.create_computation("reshape");
729
730 let source = add_op(
731 &mut builder,
732 &mut comp,
733 OperationType::Constant(
734 ConstantValue::new(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], vec![2, 3])
735 .expect("constant must be well formed"),
736 ),
737 vec![],
738 shape(&[2, 3]),
739 );
740 let _reshaped = add_op(
741 &mut builder,
742 &mut comp,
743 OperationType::Reshape,
744 vec![source],
745 shape(&[3, 2]),
746 );
747 builder
748 .mark_terminal_operands_as_outputs(&mut comp)
749 .expect("outputs must be inferable");
750
751 let outputs = ReferenceExecutor::new()
752 .execute(&comp, ValueMap::new())
753 .expect("execution must succeed");
754
755 assert_eq!(outputs[0].shape(), &[3, 2]);
756 assert_eq!(
757 outputs[0].iter().copied().collect::<Vec<_>>(),
758 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
759 );
760 }
761
762 #[test]
764 fn executes_sum_reduction() {
765 let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
766 let mut comp = builder.create_computation("reduce");
767
768 let source = add_op(
769 &mut builder,
770 &mut comp,
771 OperationType::Constant(
772 ConstantValue::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2])
773 .expect("constant must be well formed"),
774 ),
775 vec![],
776 shape(&[2, 2]),
777 );
778 let _total = add_op(
779 &mut builder,
780 &mut comp,
781 OperationType::Reduce(ReduceOperation {
782 function: ReductionFunction::Add,
783 dimensions: vec![0, 1],
784 init_value: None,
785 }),
786 vec![source],
787 scalar_shape(),
788 );
789 builder
790 .mark_terminal_operands_as_outputs(&mut comp)
791 .expect("outputs must be inferable");
792
793 let outputs = ReferenceExecutor::new()
794 .execute(&comp, ValueMap::new())
795 .expect("execution must succeed");
796
797 assert_eq!(outputs[0].iter().copied().collect::<Vec<_>>(), vec![10.0]);
798 }
799
800 #[test]
802 fn unimplemented_operation_reports_itself() {
803 let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
804 let mut comp = builder.create_computation("unsupported");
805
806 let source = add_op(
807 &mut builder,
808 &mut comp,
809 OperationType::Constant(ConstantValue::scalar(1.0)),
810 vec![],
811 scalar_shape(),
812 );
813 let _gathered = add_op(
814 &mut builder,
815 &mut comp,
816 OperationType::Gather,
817 vec![source],
818 scalar_shape(),
819 );
820 builder
821 .mark_terminal_operands_as_outputs(&mut comp)
822 .expect("outputs must be inferable");
823
824 let error = ReferenceExecutor::new()
825 .execute(&comp, ValueMap::new())
826 .expect_err("Gather is not implemented");
827 let message = format!("{error}");
828 assert!(
829 message.contains("Gather"),
830 "the error must name the operation: {message}"
831 );
832 }
833
834 #[test]
840 fn optimization_preserves_execution_results() {
841 use super::super::optimization::OptimizationPipeline;
842 use super::super::XLACompilerConfig;
843
844 let build = || {
845 let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
846 let mut comp = builder.create_computation("preserved");
847
848 let x = add_op(
849 &mut builder,
850 &mut comp,
851 OperationType::Constant(ConstantValue::scalar(4.0)),
852 vec![],
853 scalar_shape(),
854 );
855 let one = add_op(
856 &mut builder,
857 &mut comp,
858 OperationType::Constant(ConstantValue::scalar(1.0)),
859 vec![],
860 scalar_shape(),
861 );
862 let zero = add_op(
863 &mut builder,
864 &mut comp,
865 OperationType::Constant(ConstantValue::scalar(0.0)),
866 vec![],
867 scalar_shape(),
868 );
869 let scaled = add_op(
871 &mut builder,
872 &mut comp,
873 OperationType::Multiply,
874 vec![x, one],
875 scalar_shape(),
876 );
877 let shifted = add_op(
878 &mut builder,
879 &mut comp,
880 OperationType::Add,
881 vec![scaled, zero],
882 scalar_shape(),
883 );
884 let squared_a = add_op(
885 &mut builder,
886 &mut comp,
887 OperationType::Square,
888 vec![shifted],
889 scalar_shape(),
890 );
891 let squared_b = add_op(
892 &mut builder,
893 &mut comp,
894 OperationType::Square,
895 vec![shifted],
896 scalar_shape(),
897 );
898 let _total = add_op(
899 &mut builder,
900 &mut comp,
901 OperationType::Add,
902 vec![squared_a, squared_b],
903 scalar_shape(),
904 );
905 builder
906 .mark_terminal_operands_as_outputs(&mut comp)
907 .expect("outputs must be inferable");
908 comp
909 };
910
911 let before = ReferenceExecutor::new()
912 .execute(&build(), ValueMap::new())
913 .expect("the unoptimized graph must execute");
914
915 let config = XLACompilerConfig::default();
916 let mut pipeline: OptimizationPipeline<f32> = OptimizationPipeline::new(&config);
917 let optimized = pipeline
918 .optimize(build())
919 .expect("optimization must succeed");
920
921 let after = ReferenceExecutor::new()
922 .execute(&optimized, ValueMap::new())
923 .expect("the optimized graph must execute");
924
925 assert_eq!(before[0].iter().copied().collect::<Vec<_>>(), vec![32.0]);
927 assert_eq!(
928 before[0].iter().copied().collect::<Vec<_>>(),
929 after[0].iter().copied().collect::<Vec<_>>(),
930 "optimization must not change the computed result"
931 );
932 }
933}