use std::fmt::Debug;
use scirs2_core::numeric::Float;
use std::collections::{HashMap, HashSet, VecDeque};
use std::time::Instant;
use crate::error::{OptimError, Result};
#[derive(Debug)]
pub struct ComputationGraphBuilder<T: Float + Debug + Send + Sync + 'static> {
next_op_id: usize,
next_computation_id: u64,
pub _phantom: std::marker::PhantomData<T>,
}
#[derive(Debug, Clone)]
pub struct XLAComputation<T: Float + Debug + Send + Sync + 'static> {
pub id: ComputationId,
pub operations: Vec<XLAOperation<T>>,
pub inputs: Vec<InputSpecification<T>>,
pub outputs: Vec<OutputSpecification<T>>,
pub metadata: ComputationMetadata,
pub operands: HashMap<OperandId, Operand<T>>,
pub dependencies: HashMap<OperationId, Vec<OperationId>>,
pub next_operand_id: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ComputationId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OperationId(pub usize);
#[derive(Debug, Clone)]
pub struct XLAOperation<T: Float + Debug + Send + Sync + 'static> {
pub id: OperationId,
pub op_type: OperationType,
pub inputs: Vec<OperandId>,
pub output: OperandId,
pub attributes: OperationAttributes,
pub performance: OperationPerformanceCharacteristics,
pub memory_requirements: OperationMemoryRequirements,
pub source_location: Option<SourceLocation>,
pub _phantom: std::marker::PhantomData<T>,
}
#[derive(Debug, Clone, Default)]
pub struct ConstantValue {
pub data: Vec<f64>,
pub dims: Vec<usize>,
}
impl ConstantValue {
pub fn scalar(value: f64) -> Self {
Self {
data: vec![value],
dims: Vec::new(),
}
}
pub fn new(data: Vec<f64>, dims: Vec<usize>) -> Option<Self> {
let expected: usize = dims.iter().product();
if data.len() == expected {
Some(Self { data, dims })
} else {
None
}
}
pub fn element_count(&self) -> usize {
self.data.len()
}
pub fn as_scalar(&self) -> Option<f64> {
if self.data.len() == 1 {
self.data.first().copied()
} else {
None
}
}
pub fn tensor_shape(&self) -> TensorShape {
TensorShape {
dimensions: self.dims.clone(),
dynamic_dimensions: vec![false; self.dims.len()],
element_count: self.data.len(),
tuple_shapes: Vec::new(),
}
}
pub fn is_uniform(&self, value: f64) -> bool {
!self.data.is_empty() && self.data.iter().all(|&v| v == value)
}
}
impl PartialEq for ConstantValue {
fn eq(&self, other: &Self) -> bool {
self.dims == other.dims
&& self.data.len() == other.data.len()
&& self
.data
.iter()
.zip(other.data.iter())
.all(|(a, b)| a.to_bits() == b.to_bits())
}
}
impl Eq for ConstantValue {}
impl std::hash::Hash for ConstantValue {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.dims.hash(state);
for value in &self.data {
value.to_bits().hash(state);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum OperationType {
Add,
Multiply,
Subtract,
Divide,
Maximum,
Minimum,
Abs,
Exp,
Log,
Sqrt,
Rsqrt,
Square,
Sign,
Negate,
Sin,
Cos,
Tanh,
Ceil,
Floor,
Round,
Not,
And,
Or,
Xor,
Equal,
NotEqual,
Less,
LessEqual,
Greater,
GreaterEqual,
Reshape,
Transpose,
Slice,
DynamicSlice,
Pad,
Reverse,
Broadcast,
Concatenate,
Gather,
Scatter,
Reduce(ReduceOperation),
ReduceWindow,
AllReduce(AllReduceOperation),
Dot,
DotGeneral,
MatMul,
Convolution(ConvolutionConfig),
Conditional,
While,
Call,
AllGather,
AllToAll,
CollectivePermute,
ReduceScatter,
BatchNorm,
Dropout,
Copy,
Tuple,
GetTupleElement,
Constant(ConstantValue),
Parameter,
Iota,
Custom(CustomOperation),
}
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct ReduceOperation {
pub function: ReductionFunction,
pub dimensions: Vec<usize>,
pub init_value: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub enum ReductionFunction {
Add,
Multiply,
Max,
Min,
And,
Or,
Xor,
}
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct AllReduceOperation {
pub function: ReductionFunction,
pub replica_groups: Vec<Vec<usize>>,
}
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct ConvolutionConfig {
pub strides: Vec<usize>,
pub padding: PaddingConfig,
pub dilation: Vec<usize>,
pub feature_group_count: usize,
pub batch_group_count: usize,
}
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub enum PaddingConfig {
Valid,
Same,
Explicit(Vec<(usize, usize)>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomOperation {
pub name: String,
pub custom_attributes: HashMap<String, String>,
pub backend_config: Option<String>,
}
impl std::hash::Hash for CustomOperation {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
let mut attrs: Vec<_> = self.custom_attributes.iter().collect();
attrs.sort_by_key(|&(k, _)| k);
for (k, v) in attrs {
k.hash(state);
v.hash(state);
}
self.backend_config.hash(state);
}
}
#[derive(Debug, Clone)]
pub struct Operand<T: Float + Debug + Send + Sync + 'static> {
pub id: OperandId,
pub shape: TensorShape,
pub layout: Layout,
pub dtype: DataType,
pub metadata: OperandMetadata,
pub _phantom: std::marker::PhantomData<T>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OperandId(pub usize);
#[derive(Debug, Clone, PartialEq, Default)]
pub struct TensorShape {
pub dimensions: Vec<usize>,
pub dynamic_dimensions: Vec<bool>,
pub element_count: usize,
pub tuple_shapes: Vec<TensorShape>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Layout {
pub minor_to_major: Vec<usize>,
pub tiles: Vec<Tile>,
pub memory_space: MemorySpace,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Tile {
pub dimensions: Vec<usize>,
pub stride: Vec<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MemorySpace {
Default,
Host,
Device,
Pinned,
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)]
pub enum DataType {
F16,
F32,
F64,
BF16,
S8,
S16,
S32,
S64,
U8,
U16,
U32,
U64,
Pred,
C64,
C128,
}
#[derive(Debug, Clone, Default)]
pub struct OperationAttributes {
pub attributes: HashMap<String, AttributeValue>,
pub sharding: Option<ShardingSpec>,
pub fusion_hint: Option<String>,
pub performance_hint: Option<PerformanceHint>,
}
#[derive(Debug, Clone)]
pub enum AttributeValue {
String(String),
Int(i64),
Float(f64),
Bool(bool),
IntList(Vec<i64>),
FloatList(Vec<f64>),
}
#[derive(Debug, Clone)]
pub struct ShardingSpec {
pub tile_assignment: Vec<Vec<usize>>,
pub replicated_dims: Vec<usize>,
pub manual: bool,
}
#[derive(Debug, Clone)]
pub struct PerformanceHint {
pub estimated_cost: f64,
pub memory_intensity: f64,
pub compute_intensity: f64,
pub parallelization: ParallelizationHint,
}
#[derive(Debug, Clone)]
pub enum ParallelizationHint {
Sequential,
DataParallel,
ModelParallel,
PipelineParallel,
Custom(String),
}
#[derive(Debug, Clone)]
pub struct SourceLocation {
pub file: String,
pub line: u32,
pub column: u32,
pub function: String,
}
#[derive(Debug, Clone, Default)]
pub struct OperationPerformanceCharacteristics {
pub execution_time_us: u64,
pub flop_count: u64,
pub memory_accesses: u64,
pub communication_volume: u64,
pub compute_utilization: f64,
pub memory_bandwidth_utilization: f64,
}
#[derive(Debug, Clone, Default)]
pub struct OperationMemoryRequirements {
pub input_memory: usize,
pub output_memory: usize,
pub temp_memory: usize,
pub peak_memory: usize,
pub alignment_requirements: Vec<usize>,
}
#[derive(Debug, Clone)]
pub struct InputSpecification<T: Float + Debug + Send + Sync + 'static> {
pub index: usize,
pub name: String,
pub operand: OperandId,
pub shape: TensorShape,
pub dtype: DataType,
pub layout_hint: Option<Layout>,
pub _phantom: std::marker::PhantomData<T>,
}
#[derive(Debug, Clone)]
pub struct OutputSpecification<T: Float + Debug + Send + Sync + 'static> {
pub index: usize,
pub operand: OperandId,
pub shape: TensorShape,
pub dtype: DataType,
pub layout: Layout,
pub _phantom: std::marker::PhantomData<T>,
}
#[derive(Debug, Clone, Default)]
pub struct ComputationMetadata {
pub name: String,
pub created_at: Option<Instant>,
pub source_info: HashMap<String, String>,
pub optimization_opportunities: Vec<OptimizationOpportunity>,
pub performance_hints: Vec<PerformanceHint>,
pub resource_requirements: ResourceRequirements,
}
#[derive(Debug, Clone)]
pub struct OptimizationOpportunity {
pub opportunity_type: OpportunityType,
pub affected_operations: Vec<OperationId>,
pub estimated_benefit: f64,
pub complexity: ComplexityLevel,
pub description: String,
}
#[derive(Debug, Clone)]
pub enum OpportunityType {
Fusion,
MemoryLayout,
Parallelization,
Sparsity,
Quantization,
Scheduling,
Custom(String),
}
#[derive(Debug, Clone, Copy)]
pub enum ComplexityLevel {
Low,
Medium,
High,
VeryHigh,
}
#[derive(Debug, Clone, Default)]
pub struct ResourceRequirements {
pub compute_flops: u64,
pub memory_bytes: usize,
pub communication_bytes: usize,
pub execution_time_us: u64,
}
#[derive(Debug, Clone, Default)]
pub struct OperandMetadata {
pub producer: Option<OperationId>,
pub consumers: Vec<OperationId>,
pub usage_hint: UsageHint,
pub layout_hints: Vec<LayoutHint>,
}
#[derive(Debug, Clone)]
pub struct UsageHint {
pub access_pattern: AccessPattern,
pub reuse_factor: f64,
pub lifetime: OperandLifetime,
}
#[derive(Debug, Clone, Copy)]
pub enum AccessPattern {
Sequential,
Random,
Strided,
Broadcast,
Reduction,
}
#[derive(Debug, Clone)]
pub enum OperandLifetime {
Temporary,
Persistent,
Parameter,
Output,
}
#[derive(Debug, Clone)]
pub struct LayoutHint {
pub preferred_layout: Layout,
pub priority: f64,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct OperationDefinition {
pub name: String,
pub input_types: Vec<DataType>,
pub output_type: DataType,
pub shape_function: String,
pub performance_model: String,
}
#[derive(Debug, Clone)]
pub struct ValidationRule {
pub name: String,
pub description: String,
pub validator: String,
}
impl<T: Float + Debug + Send + Sync + 'static> XLAComputation<T> {
pub fn allocate_operand_id(&mut self) -> OperandId {
let id = OperandId(self.next_operand_id);
self.next_operand_id += 1;
id
}
pub fn next_free_operation_id(&self) -> OperationId {
let max = self
.operations
.iter()
.map(|op| op.id.0)
.max()
.map(|m| m.saturating_add(1))
.unwrap_or(0);
OperationId(max)
}
pub fn producer_of(&self, operand: OperandId) -> Option<&XLAOperation<T>> {
self.operations.iter().find(|op| op.output == operand)
}
pub fn operation_output(&self, op_id: OperationId) -> Option<OperandId> {
self.operations
.iter()
.find(|op| op.id == op_id)
.map(|op| op.output)
}
fn rebuild_inputs(&mut self) {
let mut rebuilt = Vec::with_capacity(self.inputs.len());
for operation in &self.operations {
if !matches!(operation.op_type, OperationType::Parameter) {
continue;
}
let Some(operand) = self.operands.get(&operation.output) else {
continue;
};
let index = rebuilt.len();
let name = self
.inputs
.iter()
.find(|spec| spec.operand == operation.output)
.map(|spec| spec.name.clone())
.unwrap_or_else(|| format!("param_{index}"));
let layout_hint = self
.inputs
.iter()
.find(|spec| spec.operand == operation.output)
.and_then(|spec| spec.layout_hint.clone());
rebuilt.push(InputSpecification {
index,
name,
operand: operation.output,
shape: operand.shape.clone(),
dtype: operand.dtype,
layout_hint,
_phantom: std::marker::PhantomData,
});
}
self.inputs = rebuilt;
}
pub fn replace_operand_uses(&mut self, from: OperandId, to: OperandId) {
if from == to {
return;
}
for operation in &mut self.operations {
for input in &mut operation.inputs {
if *input == from {
*input = to;
}
}
}
for output in &mut self.outputs {
if output.operand == from {
output.operand = to;
}
}
for input in &mut self.inputs {
if input.operand == from {
input.operand = to;
}
}
}
pub fn rebuild_dependencies(&mut self) {
self.rebuild_inputs();
for operand in self.operands.values_mut() {
operand.metadata.producer = None;
operand.metadata.consumers.clear();
}
for operation in &self.operations {
if let Some(operand) = self.operands.get_mut(&operation.output) {
operand.metadata.producer = Some(operation.id);
}
}
for operation in &self.operations {
for input_id in &operation.inputs {
if let Some(operand) = self.operands.get_mut(input_id) {
if !operand.metadata.consumers.contains(&operation.id) {
operand.metadata.consumers.push(operation.id);
}
}
}
}
let mut dependencies: HashMap<OperationId, Vec<OperationId>> = HashMap::new();
for operation in &self.operations {
let mut deps: Vec<OperationId> = Vec::new();
for input_id in &operation.inputs {
if let Some(producer) = self
.operands
.get(input_id)
.and_then(|operand| operand.metadata.producer)
{
if !deps.contains(&producer) {
deps.push(producer);
}
}
}
dependencies.insert(operation.id, deps);
}
self.dependencies = dependencies;
}
}
impl<T: Float + Debug + Default + std::fmt::Debug + Clone + Send + Sync> Default
for ComputationGraphBuilder<T>
{
fn default() -> Self {
Self::new()
}
}
impl<T: Float + Debug + Default + std::fmt::Debug + Clone + Send + Sync>
ComputationGraphBuilder<T>
{
pub fn new() -> Self {
Self {
next_op_id: 0,
next_computation_id: 0,
_phantom: std::marker::PhantomData,
}
}
pub fn create_computation(&mut self, name: &str) -> XLAComputation<T> {
let id = ComputationId(self.next_computation_id);
self.next_computation_id += 1;
XLAComputation {
id,
operations: Vec::new(),
inputs: Vec::new(),
outputs: Vec::new(),
metadata: ComputationMetadata {
name: name.to_string(),
created_at: Some(Instant::now()),
..Default::default()
},
operands: HashMap::new(),
dependencies: HashMap::new(),
next_operand_id: 0,
}
}
pub fn add_operation(
&mut self,
computation: &mut XLAComputation<T>,
op_type: OperationType,
inputs: Vec<OperandId>,
output_shape: TensorShape,
) -> Result<OperationId> {
for input_id in &inputs {
if !computation.operands.contains_key(input_id) {
return Err(OptimError::from(format!(
"Operation input operand {:?} does not exist in computation '{}'",
input_id, computation.metadata.name
)));
}
}
let op_id = OperationId(self.next_op_id);
self.next_op_id += 1;
let output_operand_id = OperandId(computation.next_operand_id);
computation.next_operand_id += 1;
let output_operand = Operand {
id: output_operand_id,
shape: output_shape,
layout: Layout::default(),
dtype: DataType::F32, metadata: OperandMetadata {
producer: Some(op_id),
..Default::default()
},
_phantom: std::marker::PhantomData,
};
computation
.operands
.insert(output_operand_id, output_operand);
for &input_id in &inputs {
if let Some(operand) = computation.operands.get_mut(&input_id) {
if !operand.metadata.consumers.contains(&op_id) {
operand.metadata.consumers.push(op_id);
}
}
}
let mut input_ops: Vec<OperationId> = Vec::new();
for &operand_id in &inputs {
if let Some(producer) = computation
.operands
.get(&operand_id)
.and_then(|operand| operand.metadata.producer)
{
if !input_ops.contains(&producer) {
input_ops.push(producer);
}
}
}
let operation = XLAOperation {
id: op_id,
op_type,
inputs,
output: output_operand_id,
attributes: OperationAttributes::default(),
performance: OperationPerformanceCharacteristics::default(),
memory_requirements: OperationMemoryRequirements::default(),
source_location: None,
_phantom: std::marker::PhantomData,
};
let is_parameter = matches!(operation.op_type, OperationType::Parameter);
computation.operations.push(operation);
computation.dependencies.insert(op_id, input_ops);
if is_parameter {
computation.rebuild_inputs();
}
Ok(op_id)
}
pub fn set_outputs(
&self,
computation: &mut XLAComputation<T>,
outputs: &[OperandId],
) -> Result<()> {
let mut specs = Vec::with_capacity(outputs.len());
for (index, &operand_id) in outputs.iter().enumerate() {
let operand = computation.operands.get(&operand_id).ok_or_else(|| {
OptimError::from(format!(
"Cannot mark unknown operand {:?} as output of computation '{}'",
operand_id, computation.metadata.name
))
})?;
specs.push(OutputSpecification {
index,
operand: operand_id,
shape: operand.shape.clone(),
dtype: operand.dtype,
layout: operand.layout.clone(),
_phantom: std::marker::PhantomData,
});
}
for &operand_id in outputs {
if let Some(operand) = computation.operands.get_mut(&operand_id) {
operand.metadata.usage_hint.lifetime = OperandLifetime::Output;
}
}
computation.outputs = specs;
Ok(())
}
pub fn mark_terminal_operands_as_outputs(
&self,
computation: &mut XLAComputation<T>,
) -> Result<usize> {
let consumed: HashSet<OperandId> = computation
.operations
.iter()
.flat_map(|op| op.inputs.iter().copied())
.collect();
let terminals: Vec<OperandId> = computation
.operations
.iter()
.map(|op| op.output)
.filter(|operand_id| !consumed.contains(operand_id))
.collect();
self.set_outputs(computation, &terminals)?;
Ok(terminals.len())
}
pub fn validate_computation(&self, computation: &XLAComputation<T>) -> Result<()> {
self.check_for_cycles(computation)?;
if !computation.operations.is_empty() && computation.outputs.is_empty() {
return Err(OptimError::from(format!(
"Computation '{}' declares no outputs; call set_outputs or \
mark_terminal_operands_as_outputs before optimization",
computation.metadata.name
)));
}
for output in &computation.outputs {
if !computation.operands.contains_key(&output.operand) {
return Err(OptimError::from(format!(
"Computation '{}' output {} references unknown operand {:?}",
computation.metadata.name, output.index, output.operand
)));
}
}
self.check_shape_compatibility(computation)?;
self.check_resource_requirements(computation)?;
Ok(())
}
fn check_for_cycles(&self, computation: &XLAComputation<T>) -> Result<()> {
for operation in &computation.operations {
if let Some(cycle_at) = Self::find_cycle_from(computation, operation.id) {
return Err(OptimError::from(format!(
"Cycle detected in computation graph '{}' involving operation {:?}",
computation.metadata.name, cycle_at
)));
}
}
Ok(())
}
fn find_cycle_from(computation: &XLAComputation<T>, start: OperationId) -> Option<OperationId> {
enum Step {
Enter(OperationId),
Leave(OperationId),
}
let mut visited: HashSet<OperationId> = HashSet::new();
let mut on_stack: HashSet<OperationId> = HashSet::new();
let mut stack: Vec<Step> = vec![Step::Enter(start)];
while let Some(step) = stack.pop() {
match step {
Step::Leave(op_id) => {
on_stack.remove(&op_id);
}
Step::Enter(op_id) => {
if on_stack.contains(&op_id) {
return Some(op_id);
}
if !visited.insert(op_id) {
continue;
}
on_stack.insert(op_id);
stack.push(Step::Leave(op_id));
if let Some(dependencies) = computation.dependencies.get(&op_id) {
for &dep_id in dependencies {
if on_stack.contains(&dep_id) {
return Some(dep_id);
}
if !visited.contains(&dep_id) {
stack.push(Step::Enter(dep_id));
}
}
}
}
}
}
None
}
fn check_shape_compatibility(&self, _computation: &XLAComputation<T>) -> Result<()> {
Ok(())
}
fn check_resource_requirements(&self, _computation: &XLAComputation<T>) -> Result<()> {
Ok(())
}
pub fn get_topological_order(
&self,
computation: &XLAComputation<T>,
) -> Result<Vec<OperationId>> {
let mut in_degree = HashMap::new();
let mut adj_list = HashMap::new();
for operation in &computation.operations {
in_degree.insert(operation.id, 0);
adj_list.insert(operation.id, Vec::new());
}
for (op_id, dependencies) in &computation.dependencies {
if !in_degree.contains_key(op_id) {
continue;
}
for &dep_id in dependencies {
let Some(neighbors) = adj_list.get_mut(&dep_id) else {
continue;
};
neighbors.push(*op_id);
if let Some(degree) = in_degree.get_mut(op_id) {
*degree += 1;
}
}
}
let mut queue = VecDeque::new();
let mut result = Vec::new();
for operation in &computation.operations {
if in_degree.get(&operation.id) == Some(&0usize) {
queue.push_back(operation.id);
}
}
while let Some(op_id) = queue.pop_front() {
result.push(op_id);
if let Some(neighbors) = adj_list.get(&op_id) {
for &neighbor in neighbors.iter() {
let Some(degree) = in_degree.get_mut(&neighbor) else {
continue;
};
*degree = degree.saturating_sub(1);
if *degree == 0 {
queue.push_back(neighbor);
}
}
}
}
if result.len() != computation.operations.len() {
return Err(OptimError::from("Graph contains cycles".to_string()));
}
Ok(result)
}
}
impl Default for Layout {
fn default() -> Self {
Self {
minor_to_major: vec![0, 1], tiles: Vec::new(),
memory_space: MemorySpace::Default,
}
}
}
impl Default for UsageHint {
fn default() -> Self {
Self {
access_pattern: AccessPattern::Sequential,
reuse_factor: 1.0,
lifetime: OperandLifetime::Temporary,
}
}
}
#[cfg(test)]
pub(crate) mod test_support {
use super::*;
pub fn scalar_shape() -> TensorShape {
TensorShape {
dimensions: Vec::new(),
dynamic_dimensions: Vec::new(),
element_count: 1,
tuple_shapes: Vec::new(),
}
}
pub fn shape(dims: &[usize]) -> TensorShape {
TensorShape {
dimensions: dims.to_vec(),
dynamic_dimensions: vec![false; dims.len()],
element_count: dims.iter().product::<usize>().max(1),
tuple_shapes: Vec::new(),
}
}
pub fn add_op<T>(
builder: &mut ComputationGraphBuilder<T>,
computation: &mut XLAComputation<T>,
op_type: OperationType,
inputs: Vec<OperandId>,
out_shape: TensorShape,
) -> OperandId
where
T: Float + Debug + Default + Clone + Send + Sync,
{
let op_id = builder
.add_operation(computation, op_type, inputs, out_shape)
.expect("test graph construction must succeed");
computation
.operation_output(op_id)
.expect("freshly added operation must have an output operand")
}
}
#[cfg(test)]
mod tests {
use super::test_support::*;
use super::*;
#[test]
fn test_computation_creation() {
let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
let computation = builder.create_computation("test_computation");
assert_eq!(computation.metadata.name, "test_computation");
}
#[test]
fn test_operation_addition() {
let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
let mut computation = builder.create_computation("test");
let result = builder.add_operation(
&mut computation,
OperationType::Parameter,
vec![],
shape(&[10, 10]),
);
assert!(result.is_ok());
assert_eq!(computation.operations.len(), 1);
}
#[test]
fn parameters_are_recorded_as_declared_inputs() {
let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
let mut computation = builder.create_computation("two_params");
let first = add_op(
&mut builder,
&mut computation,
OperationType::Parameter,
vec![],
shape(&[4]),
);
let second = add_op(
&mut builder,
&mut computation,
OperationType::Parameter,
vec![],
shape(&[4]),
);
add_op(
&mut builder,
&mut computation,
OperationType::Add,
vec![first, second],
shape(&[4]),
);
assert_eq!(computation.inputs.len(), 2, "only parameters are inputs");
assert_eq!(computation.inputs[0].index, 0);
assert_eq!(computation.inputs[1].index, 1);
assert_eq!(computation.inputs[0].operand, first);
assert_eq!(computation.inputs[1].operand, second);
assert_ne!(computation.inputs[0].operand, computation.inputs[1].operand);
assert_eq!(computation.inputs[0].shape.dimensions, vec![4]);
}
#[test]
fn producer_and_consumers_are_recorded() {
let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
let mut comp = builder.create_computation("deps");
let a = add_op(
&mut builder,
&mut comp,
OperationType::Parameter,
vec![],
scalar_shape(),
);
let b = add_op(
&mut builder,
&mut comp,
OperationType::Parameter,
vec![],
scalar_shape(),
);
let sum = add_op(
&mut builder,
&mut comp,
OperationType::Add,
vec![a, b],
scalar_shape(),
);
let sum_op = comp
.producer_of(sum)
.expect("sum operand must have a producer");
assert_eq!(
comp.operands
.get(&a)
.map(|operand| operand.metadata.consumers.clone()),
Some(vec![sum_op.id])
);
assert_eq!(
comp.operands
.get(&b)
.map(|operand| operand.metadata.consumers.clone()),
Some(vec![sum_op.id])
);
let deps = comp
.dependencies
.get(&sum_op.id)
.cloned()
.unwrap_or_default();
assert_eq!(deps.len(), 2, "add must depend on both parameters");
}
#[test]
fn operand_ids_are_never_reused() {
let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
let mut comp = builder.create_computation("ids");
let a = add_op(
&mut builder,
&mut comp,
OperationType::Parameter,
vec![],
scalar_shape(),
);
let b = add_op(
&mut builder,
&mut comp,
OperationType::Parameter,
vec![],
scalar_shape(),
);
comp.operands.remove(&b);
let c = add_op(
&mut builder,
&mut comp,
OperationType::Parameter,
vec![],
scalar_shape(),
);
assert_ne!(c, a);
assert_ne!(c, b, "a freed operand id must not be handed out again");
}
#[test]
fn terminal_operands_become_outputs() {
let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
let mut comp = builder.create_computation("outputs");
let a = add_op(
&mut builder,
&mut comp,
OperationType::Parameter,
vec![],
scalar_shape(),
);
let b = add_op(
&mut builder,
&mut comp,
OperationType::Parameter,
vec![],
scalar_shape(),
);
let sum = add_op(
&mut builder,
&mut comp,
OperationType::Add,
vec![a, b],
scalar_shape(),
);
let count = builder
.mark_terminal_operands_as_outputs(&mut comp)
.expect("marking terminal operands must succeed");
assert_eq!(count, 1);
assert_eq!(comp.outputs.len(), 1);
assert_eq!(comp.outputs[0].operand, sum);
}
#[test]
fn validation_rejects_output_less_graph() {
let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
let mut comp = builder.create_computation("no_outputs");
let _ = add_op(
&mut builder,
&mut comp,
OperationType::Parameter,
vec![],
scalar_shape(),
);
let err = builder
.validate_computation(&comp)
.expect_err("a graph with no declared outputs must be rejected");
assert!(
format!("{err}").contains("no outputs"),
"unexpected error: {err}"
);
}
#[test]
fn empty_computation_validates() {
let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
let computation = builder.create_computation("test");
assert!(builder.validate_computation(&computation).is_ok());
}
#[test]
fn topological_order_respects_dependencies() {
let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
let mut comp = builder.create_computation("topo");
let a = add_op(
&mut builder,
&mut comp,
OperationType::Parameter,
vec![],
scalar_shape(),
);
let b = add_op(
&mut builder,
&mut comp,
OperationType::Parameter,
vec![],
scalar_shape(),
);
let sum = add_op(
&mut builder,
&mut comp,
OperationType::Add,
vec![a, b],
scalar_shape(),
);
let squared = add_op(
&mut builder,
&mut comp,
OperationType::Square,
vec![sum],
scalar_shape(),
);
let order = builder
.get_topological_order(&comp)
.expect("acyclic graph must sort");
assert_eq!(order.len(), 4);
let position = |operand: OperandId| {
let op_id = comp
.producer_of(operand)
.map(|op| op.id)
.expect("operand must have a producer");
order
.iter()
.position(|&id| id == op_id)
.expect("every operation appears in the order")
};
assert!(position(a) < position(sum));
assert!(position(b) < position(sum));
assert!(position(sum) < position(squared));
}
#[test]
fn cycle_detection_catches_planted_cycle() {
let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
let mut comp = builder.create_computation("cyclic");
let a = add_op(
&mut builder,
&mut comp,
OperationType::Parameter,
vec![],
scalar_shape(),
);
let b = add_op(
&mut builder,
&mut comp,
OperationType::Square,
vec![a],
scalar_shape(),
);
let first = comp
.producer_of(a)
.map(|op| op.id)
.expect("operand a has a producer");
let second = comp
.producer_of(b)
.map(|op| op.id)
.expect("operand b has a producer");
comp.dependencies.insert(first, vec![second]);
let err = builder
.validate_computation(&comp)
.expect_err("a cyclic dependency graph must be rejected");
assert!(
format!("{err}").contains("Cycle"),
"unexpected error: {err}"
);
assert!(
builder.get_topological_order(&comp).is_err(),
"topological sort must fail on a cyclic graph"
);
}
#[test]
fn constant_payload_survives_clone_and_compares_by_value() {
let original = OperationType::Constant(ConstantValue::scalar(2.5));
let cloned = original.clone();
assert_eq!(original, cloned, "cloning must preserve the payload");
match cloned {
OperationType::Constant(value) => {
assert_eq!(value.as_scalar(), Some(2.5));
}
other => panic!("clone changed the variant: {other:?}"),
}
assert_ne!(
OperationType::Constant(ConstantValue::scalar(1.0)),
OperationType::Constant(ConstantValue::scalar(2.0)),
"different constants must not compare equal"
);
assert_eq!(
OperationType::Constant(ConstantValue::scalar(1.0)),
OperationType::Constant(ConstantValue::scalar(1.0)),
"identical constants must compare equal"
);
}
#[test]
fn constant_hash_agrees_with_equality() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let hash_of = |op: &OperationType| {
let mut hasher = DefaultHasher::new();
op.hash(&mut hasher);
hasher.finish()
};
let a = OperationType::Constant(ConstantValue::scalar(3.25));
let b = OperationType::Constant(ConstantValue::scalar(3.25));
let c = OperationType::Constant(ConstantValue::scalar(4.0));
assert_eq!(hash_of(&a), hash_of(&b));
assert_ne!(hash_of(&a), hash_of(&c));
}
#[test]
fn add_operation_rejects_unknown_input_operand() {
let mut builder: ComputationGraphBuilder<f32> = ComputationGraphBuilder::new();
let mut comp = builder.create_computation("dangling");
let result = builder.add_operation(
&mut comp,
OperationType::Square,
vec![OperandId(999)],
scalar_shape(),
);
assert!(result.is_err(), "dangling operand inputs must be rejected");
}
}