use std::fmt::Debug;
use scirs2_core::ndarray::{Array1, Array2, Dimension};
use scirs2_core::numeric::Float;
use std::collections::{HashMap, HashSet};
use super::graph_capture::{
ConvolutionConfig, DataType, OperandId, OperationType, ReduceOperation, TensorShape,
XLAComputation, XLAOperation,
};
use crate::error::{OptimError, Result};
pub struct ShapeInference {
inference_rules: HashMap<String, ShapeInferenceRule>,
broadcasting_rules: Vec<BroadcastingRule>,
constraints: Vec<ShapeConstraint>,
dynamic_shapes: HashMap<OperandId, DynamicShapeInfo>,
}
#[derive(Debug, Clone)]
pub struct ShapeInferenceRule {
pub name: String,
pub operation_type: String,
pub input_requirements: Vec<ShapeRequirement>,
pub output_shape_fn: String,
pub constraints: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ShapeRequirement {
pub input_index: usize,
pub required_rank: Option<usize>,
pub required_dimensions: Vec<Option<usize>>,
pub data_type_requirements: Vec<DataType>,
pub additional_constraints: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct BroadcastingRule {
pub name: String,
pub compatible_patterns: Vec<ShapePattern>,
pub result_shape_fn: String,
}
#[derive(Debug, Clone)]
pub enum ShapePattern {
Exact(Vec<usize>),
BroadcastCompatible,
Prefix(Vec<usize>),
Suffix(Vec<usize>),
AnyRank(usize),
Any,
}
#[derive(Debug, Clone)]
pub struct ShapeConstraint {
pub name: String,
pub constraint_type: ConstraintType,
pub operands: Vec<OperandId>,
pub parameters: HashMap<String, String>,
pub error_message: String,
}
#[derive(Debug, Clone)]
pub enum ConstraintType {
IdenticalShapes,
BroadcastCompatible,
MatchingRanks,
DimensionMatch { dims: Vec<usize> },
ElementCountMatch,
CustomPredicate(String),
}
#[derive(Debug, Clone)]
pub struct DynamicShapeInfo {
pub static_dimensions: HashMap<usize, usize>,
pub dynamic_constraints: Vec<DynamicConstraint>,
pub upper_bounds: HashMap<usize, usize>,
pub lower_bounds: HashMap<usize, usize>,
}
#[derive(Debug, Clone)]
pub struct DynamicConstraint {
pub constraint_type: DynamicConstraintType,
pub dimensions: Vec<usize>,
pub parameters: HashMap<String, i64>,
}
#[derive(Debug, Clone)]
pub enum DynamicConstraintType {
Equal,
Divisible,
Range,
Multiple,
}
#[derive(Debug)]
pub struct ShapeInferenceContext<T: Float + Debug + Send + Sync + 'static> {
pub computation: XLAComputation<T>,
pub inferred_shapes: HashMap<OperandId, InferredShape>,
pub discovered_constraints: Vec<ShapeConstraint>,
pub dynamic_info: HashMap<OperandId, DynamicShapeInfo>,
pub options: ShapeInferenceOptions,
}
#[derive(Debug, Clone)]
pub struct InferredShape {
pub static_shape: Option<TensorShape>,
pub dynamic_shape: Option<DynamicShapeTemplate>,
pub confidence: f64,
pub inference_method: String,
pub alternatives: Vec<TensorShape>,
}
#[derive(Debug, Clone)]
pub struct DynamicShapeTemplate {
pub template_dims: Vec<Option<usize>>,
pub symbols: HashMap<usize, String>,
pub expressions: HashMap<usize, String>,
}
#[derive(Debug, Clone)]
pub struct ShapeInferenceOptions {
pub allow_dynamic_shapes: bool,
pub strict_mode: bool,
pub max_iterations: usize,
pub enable_optimization: bool,
pub backward_propagation: bool,
}
impl Default for ShapeInference {
fn default() -> Self {
Self::new()
}
}
impl ShapeInference {
pub fn new() -> Self {
let mut inference = Self {
inference_rules: HashMap::new(),
broadcasting_rules: Vec::new(),
constraints: Vec::new(),
dynamic_shapes: HashMap::new(),
};
inference.initialize_builtin_rules();
inference
}
fn initialize_builtin_rules(&mut self) {
self.add_inference_rule(ShapeInferenceRule {
name: "elementwise_binary".to_string(),
operation_type: "Add".to_string(),
input_requirements: vec![
ShapeRequirement {
input_index: 0,
required_rank: None,
required_dimensions: vec![],
data_type_requirements: vec![],
additional_constraints: vec![],
},
ShapeRequirement {
input_index: 1,
required_rank: None,
required_dimensions: vec![],
data_type_requirements: vec![],
additional_constraints: vec!["broadcast_compatible_with_input_0".to_string()],
},
],
output_shape_fn: "broadcast_result".to_string(),
constraints: vec!["inputs_broadcast_compatible".to_string()],
});
self.add_inference_rule(ShapeInferenceRule {
name: "dot_product".to_string(),
operation_type: "Dot".to_string(),
input_requirements: vec![
ShapeRequirement {
input_index: 0,
required_rank: Some(2),
required_dimensions: vec![None, None],
data_type_requirements: vec![],
additional_constraints: vec![],
},
ShapeRequirement {
input_index: 1,
required_rank: Some(2),
required_dimensions: vec![None, None],
data_type_requirements: vec![],
additional_constraints: vec!["inner_dimension_matches".to_string()],
},
],
output_shape_fn: "matrix_multiply_result".to_string(),
constraints: vec!["inner_dimensions_match".to_string()],
});
self.add_broadcasting_rule(BroadcastingRule {
name: "standard_broadcast".to_string(),
compatible_patterns: vec![ShapePattern::BroadcastCompatible],
result_shape_fn: "broadcast_shapes".to_string(),
});
}
pub fn infer_shapes<T: Float + Default + std::fmt::Debug + Clone + Send + Sync + 'static>(
computation: XLAComputation<T>,
) -> Result<XLAComputation<T>> {
let mut inference = Self::new();
let options = ShapeInferenceOptions::default();
let mut context = ShapeInferenceContext {
computation,
inferred_shapes: HashMap::new(),
discovered_constraints: Vec::new(),
dynamic_info: HashMap::new(),
options,
};
inference.run_inference(&mut context)
}
fn run_inference<T: Float + Default + std::fmt::Debug + Clone + Send + Sync + 'static>(
&mut self,
context: &mut ShapeInferenceContext<T>,
) -> Result<XLAComputation<T>> {
self.initialize_input_shapes(context)?;
for iteration in 0..context.options.max_iterations {
let mut changed = false;
let operations = context.computation.operations.clone();
for operation in &operations {
if self.infer_operation_shape(operation, context)? {
changed = true;
}
}
if !changed {
break;
}
self.validate_constraints(context)?;
}
self.finalize_shapes(context)
}
fn initialize_input_shapes<
T: Float + Default + std::fmt::Debug + Clone + Send + Sync + 'static,
>(
&self,
context: &mut ShapeInferenceContext<T>,
) -> Result<()> {
for input_spec in &context.computation.inputs {
let inferred = InferredShape {
static_shape: Some(input_spec.shape.clone()),
dynamic_shape: None,
confidence: 1.0,
inference_method: "input_specification".to_string(),
alternatives: vec![],
};
for (operand_id, operand) in &context.computation.operands {
if operand.shape == input_spec.shape {
context
.inferred_shapes
.insert(*operand_id, inferred.clone());
break;
}
}
}
Ok(())
}
fn infer_operation_shape<
T: Float + Default + std::fmt::Debug + Clone + Send + Sync + 'static,
>(
&self,
operation: &XLAOperation<T>,
context: &mut ShapeInferenceContext<T>,
) -> Result<bool> {
if context.inferred_shapes.contains_key(&operation.output) {
return Ok(false);
}
let input_shapes: Vec<Option<&InferredShape>> = operation
.inputs
.iter()
.map(|&operand_id| context.inferred_shapes.get(&operand_id))
.collect();
if input_shapes.iter().any(|shape| shape.is_none()) {
return Ok(false); }
let output_shape = match &operation.op_type {
OperationType::Add
| OperationType::Multiply
| OperationType::Subtract
| OperationType::Divide => self.infer_elementwise_shape(&input_shapes, context)?,
OperationType::Dot => self.infer_dot_shape(&input_shapes, context)?,
OperationType::Reshape => {
self.infer_reshape_shape(operation, &input_shapes, context)?
}
OperationType::Transpose => {
self.infer_transpose_shape(operation, &input_shapes, context)?
}
OperationType::Reduce(reduce_op) => {
self.infer_reduce_shape(reduce_op, &input_shapes, context)?
}
OperationType::Convolution(conv_config) => {
self.infer_convolution_shape(conv_config, &input_shapes, context)?
}
OperationType::Constant(_value) => {
InferredShape {
static_shape: Some(TensorShape {
dimensions: vec![],
dynamic_dimensions: vec![],
element_count: 1,
tuple_shapes: vec![],
}),
dynamic_shape: None,
confidence: 1.0,
inference_method: "Direct".to_string(),
alternatives: vec![],
}
}
_ => {
if let Some(Some(first_input)) = input_shapes.first() {
(*first_input).clone()
} else {
InferredShape {
static_shape: None,
dynamic_shape: None,
confidence: 0.0,
inference_method: "Unknown".to_string(),
alternatives: vec![],
}
}
}
};
context
.inferred_shapes
.insert(operation.output, output_shape);
Ok(true)
}
fn infer_elementwise_shape<
T: Float + Default + std::fmt::Debug + Clone + Send + Sync + 'static,
>(
&self,
input_shapes: &[Option<&InferredShape>],
_context: &ShapeInferenceContext<T>,
) -> Result<InferredShape> {
if input_shapes.len() != 2 {
return Err(OptimError::from(
"Elementwise operations require exactly 2 inputs".to_string(),
));
}
let shape1 = input_shapes[0].expect("unwrap failed");
let shape2 = input_shapes[1].expect("unwrap failed");
if let (Some(static1), Some(static2)) = (&shape1.static_shape, &shape2.static_shape) {
let result_shape = self.broadcast_shapes(static1, static2)?;
Ok(InferredShape {
static_shape: Some(result_shape),
dynamic_shape: None,
confidence: (shape1.confidence * shape2.confidence).min(1.0),
inference_method: "elementwise_broadcast".to_string(),
alternatives: vec![],
})
} else {
Ok(InferredShape {
static_shape: None,
dynamic_shape: None,
confidence: 0.5,
inference_method: "elementwise_dynamic".to_string(),
alternatives: vec![],
})
}
}
fn infer_dot_shape<T: Float + Default + std::fmt::Debug + Clone + Send + Sync + 'static>(
&self,
input_shapes: &[Option<&InferredShape>],
_context: &ShapeInferenceContext<T>,
) -> Result<InferredShape> {
if input_shapes.len() != 2 {
return Err(OptimError::from(
"Dot operations require exactly 2 inputs".to_string(),
));
}
let shape1 = input_shapes[0].expect("unwrap failed");
let shape2 = input_shapes[1].expect("unwrap failed");
if let (Some(static1), Some(static2)) = (&shape1.static_shape, &shape2.static_shape) {
if static1.dimensions.len() == 2 && static2.dimensions.len() == 2 {
if static1.dimensions[1] != static2.dimensions[0] {
return Err(OptimError::from(format!(
"Incompatible dimensions for dot product: {} vs {}",
static1.dimensions[1], static2.dimensions[0]
)));
}
let result_shape = TensorShape {
dimensions: vec![static1.dimensions[0], static2.dimensions[1]],
dynamic_dimensions: vec![false, false],
element_count: static1.dimensions[0] * static2.dimensions[1],
tuple_shapes: vec![],
};
Ok(InferredShape {
static_shape: Some(result_shape),
dynamic_shape: None,
confidence: (shape1.confidence * shape2.confidence).min(1.0),
inference_method: "matrix_multiply".to_string(),
alternatives: vec![],
})
} else {
Err(OptimError::from(
"Dot product requires 2D tensors".to_string(),
))
}
} else {
Ok(InferredShape {
static_shape: None,
dynamic_shape: None,
confidence: 0.5,
inference_method: "dot_dynamic".to_string(),
alternatives: vec![],
})
}
}
fn infer_reshape_shape<T: Float + Default + std::fmt::Debug + Clone + Send + Sync + 'static>(
&self,
_operation: &XLAOperation<T>,
input_shapes: &[Option<&InferredShape>],
_context: &ShapeInferenceContext<T>,
) -> Result<InferredShape> {
if let Some(Some(input_shape)) = input_shapes.first() {
Ok((*input_shape).clone())
} else {
Err(OptimError::from("Reshape requires input shape".to_string()))
}
}
fn infer_transpose_shape<
T: Float + Default + std::fmt::Debug + Clone + Send + Sync + 'static,
>(
&self,
_operation: &XLAOperation<T>,
input_shapes: &[Option<&InferredShape>],
_context: &ShapeInferenceContext<T>,
) -> Result<InferredShape> {
if let Some(Some(input_shape)) = input_shapes.first() {
if let Some(static_shape) = &input_shape.static_shape {
let mut new_dims = static_shape.dimensions.clone();
new_dims.reverse();
let result_shape = TensorShape {
dimensions: new_dims,
dynamic_dimensions: static_shape
.dynamic_dimensions
.iter()
.rev()
.cloned()
.collect(),
element_count: static_shape.element_count,
tuple_shapes: vec![],
};
Ok(InferredShape {
static_shape: Some(result_shape),
dynamic_shape: None,
confidence: input_shape.confidence,
inference_method: "transpose".to_string(),
alternatives: vec![],
})
} else {
Ok((*input_shape).clone())
}
} else {
Err(OptimError::from(
"Transpose requires input shape".to_string(),
))
}
}
fn infer_reduce_shape<T: Float + Default + std::fmt::Debug + Clone + Send + Sync + 'static>(
&self,
reduce_op: &ReduceOperation,
input_shapes: &[Option<&InferredShape>],
_context: &ShapeInferenceContext<T>,
) -> Result<InferredShape> {
if let Some(Some(input_shape)) = input_shapes.first() {
if let Some(static_shape) = &input_shape.static_shape {
let mut result_dims = static_shape.dimensions.clone();
let mut result_dynamic = static_shape.dynamic_dimensions.clone();
let mut sorted_dims = reduce_op.dimensions.clone();
sorted_dims.sort_by(|a, b| b.cmp(a));
for &dim in &sorted_dims {
if dim < result_dims.len() {
result_dims.remove(dim);
result_dynamic.remove(dim);
}
}
let element_count = result_dims.iter().product();
let result_shape = TensorShape {
dimensions: result_dims,
dynamic_dimensions: result_dynamic,
element_count,
tuple_shapes: vec![],
};
Ok(InferredShape {
static_shape: Some(result_shape),
dynamic_shape: None,
confidence: input_shape.confidence,
inference_method: "reduce".to_string(),
alternatives: vec![],
})
} else {
Ok((*input_shape).clone())
}
} else {
Err(OptimError::from("Reduce requires input shape".to_string()))
}
}
fn infer_convolution_shape<
T: Float + Default + std::fmt::Debug + Clone + Send + Sync + 'static,
>(
&self,
_conv_config: &ConvolutionConfig,
input_shapes: &[Option<&InferredShape>],
_context: &ShapeInferenceContext<T>,
) -> Result<InferredShape> {
if let Some(Some(input_shape)) = input_shapes.first() {
Ok((*input_shape).clone())
} else {
Err(OptimError::from(
"Convolution requires input shape".to_string(),
))
}
}
fn infer_constant_shape<
T: Float + Default + std::fmt::Debug + Clone + Send + Sync + 'static,
>(
&self,
_value: &T,
_context: &ShapeInferenceContext<T>,
) -> Result<InferredShape> {
let result_shape = TensorShape {
dimensions: vec![],
dynamic_dimensions: vec![],
element_count: 1,
tuple_shapes: vec![],
};
Ok(InferredShape {
static_shape: Some(result_shape),
dynamic_shape: None,
confidence: 1.0,
inference_method: "constant".to_string(),
alternatives: vec![],
})
}
fn broadcast_shapes(&self, shape1: &TensorShape, shape2: &TensorShape) -> Result<TensorShape> {
let dims1 = &shape1.dimensions;
let dims2 = &shape2.dimensions;
let max_rank = dims1.len().max(dims2.len());
let mut result_dims = Vec::with_capacity(max_rank);
for i in 0..max_rank {
let dim1 = if i < dims1.len() {
dims1[dims1.len() - 1 - i]
} else {
1
};
let dim2 = if i < dims2.len() {
dims2[dims2.len() - 1 - i]
} else {
1
};
if dim1 == dim2 {
result_dims.push(dim1);
} else if dim1 == 1 {
result_dims.push(dim2);
} else if dim2 == 1 {
result_dims.push(dim1);
} else {
return Err(OptimError::from(format!(
"Incompatible dimensions for broadcasting: {} vs {}",
dim1, dim2
)));
}
}
result_dims.reverse();
let element_count = result_dims.iter().product();
Ok(TensorShape {
dimensions: result_dims,
dynamic_dimensions: vec![false; max_rank],
element_count,
tuple_shapes: vec![],
})
}
fn validate_constraints<
T: Float + Default + std::fmt::Debug + Clone + Send + Sync + 'static,
>(
&self,
_context: &ShapeInferenceContext<T>,
) -> Result<()> {
Ok(())
}
fn finalize_shapes<T: Float + Default + std::fmt::Debug + Clone + Send + Sync + 'static>(
&self,
context: &mut ShapeInferenceContext<T>,
) -> Result<XLAComputation<T>> {
let mut computation = context.computation.clone();
for (operand_id, inferred) in &context.inferred_shapes {
if let Some(operand) = computation.operands.get_mut(operand_id) {
if let Some(static_shape) = &inferred.static_shape {
operand.shape = static_shape.clone();
}
}
}
Ok(computation)
}
fn add_inference_rule(&mut self, rule: ShapeInferenceRule) {
self.inference_rules.insert(rule.name.clone(), rule);
}
fn add_broadcasting_rule(&mut self, rule: BroadcastingRule) {
self.broadcasting_rules.push(rule);
}
}
impl Default for ShapeInferenceOptions {
fn default() -> Self {
Self {
allow_dynamic_shapes: true,
strict_mode: false,
max_iterations: 10,
enable_optimization: true,
backward_propagation: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shape_inference_creation() {
let inference = ShapeInference::new();
assert!(!inference.inference_rules.is_empty());
assert!(!inference.broadcasting_rules.is_empty());
}
#[test]
fn test_broadcast_shapes() {
let inference = ShapeInference::new();
let shape1 = TensorShape {
dimensions: vec![3, 1],
dynamic_dimensions: vec![false, false],
element_count: 3,
tuple_shapes: vec![],
};
let shape2 = TensorShape {
dimensions: vec![1, 4],
dynamic_dimensions: vec![false, false],
element_count: 4,
tuple_shapes: vec![],
};
let result = inference
.broadcast_shapes(&shape1, &shape2)
.expect("unwrap failed");
assert_eq!(result.dimensions, vec![3, 4]);
assert_eq!(result.element_count, 12);
}
#[test]
fn test_dot_shape_inference() {
let inference = ShapeInference::new();
let shape1 = InferredShape {
static_shape: Some(TensorShape {
dimensions: vec![2, 3],
dynamic_dimensions: vec![false, false],
element_count: 6,
tuple_shapes: vec![],
}),
dynamic_shape: None,
confidence: 1.0,
inference_method: "test".to_string(),
alternatives: vec![],
};
let shape2 = InferredShape {
static_shape: Some(TensorShape {
dimensions: vec![3, 4],
dynamic_dimensions: vec![false, false],
element_count: 12,
tuple_shapes: vec![],
}),
dynamic_shape: None,
confidence: 1.0,
inference_method: "test".to_string(),
alternatives: vec![],
};
let input_shapes = vec![Some(&shape1), Some(&shape2)];
let context: ShapeInferenceContext<f32> = ShapeInferenceContext {
computation: XLAComputation {
id: super::super::graph_capture::ComputationId(0),
operations: vec![],
inputs: vec![],
outputs: vec![],
metadata: super::super::graph_capture::ComputationMetadata::default(),
operands: std::collections::HashMap::new(),
dependencies: std::collections::HashMap::new(),
},
inferred_shapes: std::collections::HashMap::new(),
discovered_constraints: vec![],
dynamic_info: std::collections::HashMap::new(),
options: ShapeInferenceOptions::default(),
};
let result = inference
.infer_dot_shape(&input_shapes, &context)
.expect("unwrap failed");
let static_shape = result.static_shape.expect("unwrap failed");
assert_eq!(static_shape.dimensions, vec![2, 4]);
assert_eq!(static_shape.element_count, 8);
}
}