use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use crate::TensorDataExt;
use crate::graph_state::GraphState;
use crate::ir::{ArgType, Argument, DType, NodeType, RawNode, ValueSource};
use crate::tensor_store::TensorDataRef;
pub(crate) fn simplify_constant_shape(
mut nodes: Vec<RawNode>,
graph_outputs: &mut [Argument],
state: &Rc<RefCell<GraphState>>,
) -> Vec<RawNode> {
let mut producer: HashMap<String, usize> = HashMap::new();
for (i, node) in nodes.iter().enumerate() {
for out in &node.outputs {
producer.insert(out.name.clone(), i);
}
}
let mut gather_replacements: Vec<(usize, i64)> = Vec::new();
for (gi, node) in nodes.iter().enumerate() {
if node.node_type != NodeType::Gather || node.inputs.len() < 2 {
continue;
}
if let Some(dim_value) = extract_constant_shape_dim(node, &nodes, &producer) {
gather_replacements.push((gi, dim_value));
}
}
let mut constant_outputs: Vec<String> = Vec::new();
for (gi, dim_value) in &gather_replacements {
let gather = &nodes[*gi];
log::info!(
"Simplification: replacing Shape->Gather '{}' with constant {}",
gather.name,
dim_value,
);
let output_name = gather.outputs[0].name.clone();
let output_ty = gather.outputs[0].ty.clone();
let node_name = nodes[*gi].name.clone();
nodes[*gi] = make_constant_node(&node_name, &output_name, &[*dim_value], output_ty, state);
constant_outputs.push(output_name);
}
let mut slice_replacements: Vec<(usize, Vec<i64>)> = Vec::new();
for (si, node) in nodes.iter().enumerate() {
if node.node_type != NodeType::Slice || node.inputs.is_empty() {
continue;
}
if let Some(values) = extract_constant_shape_slice(node, &nodes, &producer) {
slice_replacements.push((si, values));
}
}
for (si, values) in &slice_replacements {
let slice_node = &nodes[*si];
log::info!(
"Simplification: replacing Shape->Slice '{}' with constant {:?}",
slice_node.name,
values,
);
let output_name = slice_node.outputs[0].name.clone();
let node_name = nodes[*si].name.clone();
nodes[*si] = make_constant_node(
&node_name,
&output_name,
values,
ArgType::Shape(values.len()),
state,
);
constant_outputs.push(output_name);
}
if !constant_outputs.is_empty() {
super::update_constant_references(&mut nodes, graph_outputs, &constant_outputs);
}
nodes
}
fn make_constant_node(
node_name: &str,
output_name: &str,
values: &[i64],
ty: ArgType,
state: &Rc<RefCell<GraphState>>,
) -> RawNode {
let bytes: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
let shape = if values.len() == 1 && matches!(ty, ArgType::ScalarNative(_)) {
vec![]
} else {
vec![values.len()]
};
let data_ref = TensorDataRef::new(bytes::Bytes::from(bytes), shape, DType::I64);
let mut gs = state.borrow_mut();
let data_id = gs.register_constant(output_name.to_string(), data_ref);
let value_store = gs.build_value_store();
let input_name = format!("{}_const", output_name);
RawNode {
node_type: NodeType::Constant,
name: node_name.to_string(),
inputs: vec![Argument {
name: input_name,
ty: ty.clone(),
value_source: ValueSource::Static(data_id),
value_store: Some(value_store.clone()),
}],
outputs: vec![Argument {
name: output_name.to_string(),
ty,
value_source: ValueSource::Constant,
value_store: Some(value_store),
}],
attrs: HashMap::new(),
}
}
fn extract_constant_shape_dim(
gather: &RawNode,
nodes: &[RawNode],
producer: &HashMap<String, usize>,
) -> Option<i64> {
match &gather.outputs[0].ty {
ArgType::ScalarNative(_) => {}
ArgType::Shape(1) => {}
_ => return None,
}
let axis = gather
.attrs
.get("axis")
.map(|v| v.clone().into_i64())
.unwrap_or(0);
if axis != 0 {
return None;
}
let shape_idx = *producer.get(&gather.inputs[0].name)?;
let shape_node = &nodes[shape_idx];
if shape_node.node_type != NodeType::Shape {
return None;
}
let static_shape = shape_node.inputs[0].ty.static_shape_known()?;
let rank = static_shape.len();
let mut start = shape_node
.attrs
.get("start")
.map(|v| v.clone().into_i64())
.unwrap_or(0);
if start < 0 {
start += rank as i64;
}
let start = start.max(0) as usize;
let mut index_val = gather.inputs[1].value()?.scalar_i64().ok()?;
let sliced_len = static_shape.len() - start;
if index_val < 0 {
index_val += sliced_len as i64;
}
if index_val < 0 || index_val as usize >= sliced_len {
return None;
}
let dim_idx = start + index_val as usize;
Some(static_shape[dim_idx] as i64)
}
fn extract_full_static_shape(shape_node: &RawNode) -> Option<Vec<i64>> {
let static_shape = shape_node.inputs[0].ty.static_shape_known()?;
let rank = static_shape.len();
let mut start = shape_node
.attrs
.get("start")
.map(|v| v.clone().into_i64())
.unwrap_or(0);
if start < 0 {
start += rank as i64;
}
let start = start.max(0) as usize;
let mut end = shape_node
.attrs
.get("end")
.map(|v| v.clone().into_i64())
.unwrap_or(rank as i64);
if end < 0 {
end += rank as i64;
}
let end = (end as usize).min(rank);
if start >= end {
return Some(vec![]);
}
Some(static_shape[start..end].iter().map(|&d| d as i64).collect())
}
fn extract_constant_shape_slice(
slice: &RawNode,
nodes: &[RawNode],
producer: &HashMap<String, usize>,
) -> Option<Vec<i64>> {
if slice.inputs.len() < 3 {
return None;
}
let shape_idx = *producer.get(&slice.inputs[0].name)?;
let shape_node = &nodes[shape_idx];
if shape_node.node_type != NodeType::Shape {
return None;
}
let shape_values = extract_full_static_shape(shape_node)?;
let shape_len = shape_values.len() as i64;
let starts = slice.inputs[1].value()?.to_i64_vec().ok()?;
let ends = slice.inputs[2].value()?.to_i64_vec().ok()?;
if starts.len() != ends.len() {
return None;
}
let axes: Vec<i64> = if slice.inputs.len() > 3 {
match slice.inputs[3].value() {
Some(data) => data.to_i64_vec().ok()?,
None => return None,
}
} else {
(0..starts.len() as i64).collect()
};
let steps: Vec<i64> = if slice.inputs.len() > 4 {
match slice.inputs[4].value() {
Some(data) => data.to_i64_vec().ok()?,
None => return None,
}
} else {
vec![1; starts.len()]
};
if axes.len() != starts.len() || steps.len() != starts.len() {
return None;
}
if axes.iter().any(|&a| a != 0) {
return None;
}
let mut start = starts[0];
let mut end = ends[0];
let step = steps[0];
if step == 0 {
return None;
}
if start < 0 {
start += shape_len;
}
if end < 0 {
end += shape_len;
}
start = start.clamp(0, shape_len);
end = end.clamp(0, shape_len);
let result: Vec<i64> = if step > 0 {
(start..end)
.step_by(step as usize)
.map(|i| shape_values[i as usize])
.collect()
} else {
let mut vals = Vec::new();
let mut i = start;
while i > end {
vals.push(shape_values[i as usize]);
i += step; }
vals
};
Some(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::{AttributeValue, TensorType};
use crate::simplify::tests::arg;
use crate::tensor_store::{TensorDataRef, TensorStore, ValueStore};
fn tensor_arg_with_shape(name: &str, shape: Vec<usize>) -> Argument {
Argument {
name: name.to_string(),
ty: ArgType::Tensor(TensorType {
dtype: DType::F32,
rank: shape.len(),
static_shape: Some(shape.into_iter().map(Some).collect()),
}),
value_source: ValueSource::Dynamic,
value_store: None,
}
}
fn shape_arg(name: &str, rank: usize) -> Argument {
Argument {
name: name.to_string(),
ty: ArgType::Shape(rank),
value_source: ValueSource::Dynamic,
value_store: None,
}
}
fn scalar_arg(name: &str, dtype: DType) -> Argument {
Argument {
name: name.to_string(),
ty: ArgType::ScalarNative(dtype),
value_source: ValueSource::Dynamic,
value_store: None,
}
}
fn const_scalar_arg(name: &str, value: i64) -> Argument {
Argument::from_const_i64(name, value)
}
fn const_i64_vec_arg(name: &str, values: &[i64]) -> Argument {
let bytes: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
let data_ref =
TensorDataRef::new(bytes::Bytes::from(bytes), vec![values.len()], DType::I64);
let mut store = TensorStore::new();
let id = store.store(data_ref);
let mut constant_map = std::collections::HashMap::new();
constant_map.insert(name.to_string(), id);
let value_store = ValueStore::new(
std::sync::Arc::new(store),
std::sync::Arc::new(constant_map),
);
Argument {
name: name.to_string(),
ty: ArgType::Shape(values.len()),
value_source: ValueSource::Constant,
value_store: Some(value_store),
}
}
fn raw_node(
name: &str,
node_type: NodeType,
inputs: Vec<Argument>,
outputs: Vec<Argument>,
attrs: HashMap<String, AttributeValue>,
) -> RawNode {
RawNode {
node_type,
name: name.to_string(),
inputs,
outputs,
attrs,
}
}
fn test_state() -> Rc<RefCell<GraphState>> {
Rc::new(RefCell::new(GraphState::new(&[], &[], &[], &[])))
}
#[test]
fn test_shape_gather_replaced_with_constant() {
let nodes = vec![
raw_node(
"shape",
NodeType::Shape,
vec![tensor_arg_with_shape("input", vec![2, 3, 4])],
vec![shape_arg("shape_out", 3)],
HashMap::new(),
),
raw_node(
"gather",
NodeType::Gather,
vec![shape_arg("shape_out", 3), const_scalar_arg("idx", 1)],
vec![scalar_arg("dim_val", DType::I64)],
[("axis".to_string(), AttributeValue::Int64(0))]
.into_iter()
.collect(),
),
];
let state = test_state();
let result = simplify_constant_shape(nodes, &mut [], &state);
let gather = result.iter().find(|n| n.name == "gather").unwrap();
assert_eq!(gather.node_type, NodeType::Constant);
let val = gather.inputs[0].value().unwrap().scalar_i64().unwrap();
assert_eq!(val, 3);
}
#[test]
fn test_shape_with_start_attr() {
let nodes = vec![
raw_node(
"shape",
NodeType::Shape,
vec![tensor_arg_with_shape("input", vec![2, 3, 4, 5])],
vec![shape_arg("shape_out", 3)],
[("start".to_string(), AttributeValue::Int64(1))]
.into_iter()
.collect(),
),
raw_node(
"gather",
NodeType::Gather,
vec![shape_arg("shape_out", 3), const_scalar_arg("idx", 1)],
vec![scalar_arg("dim_val", DType::I64)],
[("axis".to_string(), AttributeValue::Int64(0))]
.into_iter()
.collect(),
),
];
let state = test_state();
let result = simplify_constant_shape(nodes, &mut [], &state);
let gather = result.iter().find(|n| n.name == "gather").unwrap();
assert_eq!(gather.node_type, NodeType::Constant);
let val = gather.inputs[0].value().unwrap().scalar_i64().unwrap();
assert_eq!(val, 4); }
#[test]
fn test_gather_from_non_shape_not_replaced() {
let nodes = vec![
raw_node(
"relu",
NodeType::Relu,
vec![arg("input")],
vec![arg("relu_out")],
HashMap::new(),
),
raw_node(
"gather",
NodeType::Gather,
vec![arg("relu_out"), const_scalar_arg("idx", 0)],
vec![arg("out")],
[("axis".to_string(), AttributeValue::Int64(0))]
.into_iter()
.collect(),
),
];
let state = test_state();
let result = simplify_constant_shape(nodes, &mut [], &state);
assert_eq!(result[1].node_type, NodeType::Gather);
}
#[test]
fn test_shape_without_static_shape_not_replaced() {
let nodes = vec![
raw_node(
"shape",
NodeType::Shape,
vec![arg("input")], vec![shape_arg("shape_out", 2)],
HashMap::new(),
),
raw_node(
"gather",
NodeType::Gather,
vec![shape_arg("shape_out", 2), const_scalar_arg("idx", 0)],
vec![arg("dim_val")],
[("axis".to_string(), AttributeValue::Int64(0))]
.into_iter()
.collect(),
),
];
let state = test_state();
let result = simplify_constant_shape(nodes, &mut [], &state);
assert_eq!(result[1].node_type, NodeType::Gather);
}
#[test]
fn test_shape_not_replaced_without_gather_or_slice() {
let nodes = vec![
raw_node(
"shape",
NodeType::Shape,
vec![tensor_arg_with_shape("input", vec![2, 3, 4])],
vec![shape_arg("shape_out", 3)],
HashMap::new(),
),
raw_node(
"consumer",
NodeType::Add,
vec![shape_arg("shape_out", 3), arg("other")],
vec![arg("output")],
HashMap::new(),
),
];
let state = test_state();
let result = simplify_constant_shape(nodes, &mut [], &state);
let shape = result.iter().find(|n| n.name == "shape").unwrap();
assert_eq!(shape.node_type, NodeType::Shape); }
#[test]
fn test_shape_dynamic_not_replaced() {
let nodes = vec![raw_node(
"shape",
NodeType::Shape,
vec![arg("input")], vec![shape_arg("shape_out", 2)],
HashMap::new(),
)];
let state = test_state();
let result = simplify_constant_shape(nodes, &mut [], &state);
assert_eq!(result[0].node_type, NodeType::Shape);
}
#[test]
fn test_shape_slice_replaced_with_constant() {
let nodes = vec![
raw_node(
"shape",
NodeType::Shape,
vec![tensor_arg_with_shape("input", vec![2, 3, 4, 5])],
vec![shape_arg("shape_out", 4)],
HashMap::new(),
),
raw_node(
"slice",
NodeType::Slice,
vec![
shape_arg("shape_out", 4),
const_i64_vec_arg("starts", &[1]),
const_i64_vec_arg("ends", &[3]),
],
vec![shape_arg("slice_out", 2)],
HashMap::new(),
),
];
let state = test_state();
let result = simplify_constant_shape(nodes, &mut [], &state);
let slice = result.iter().find(|n| n.name == "slice").unwrap();
assert_eq!(slice.node_type, NodeType::Constant);
let vals = slice.inputs[0].value().unwrap().to_i64_vec().unwrap();
assert_eq!(vals, vec![3, 4]);
}
#[test]
fn test_shape_slice_with_step() {
let nodes = vec![
raw_node(
"shape",
NodeType::Shape,
vec![tensor_arg_with_shape("input", vec![10, 20, 30, 40, 50])],
vec![shape_arg("shape_out", 5)],
HashMap::new(),
),
raw_node(
"slice",
NodeType::Slice,
vec![
shape_arg("shape_out", 5),
const_i64_vec_arg("starts", &[0]),
const_i64_vec_arg("ends", &[5]),
const_i64_vec_arg("axes", &[0]),
const_i64_vec_arg("steps", &[2]),
],
vec![shape_arg("slice_out", 3)],
HashMap::new(),
),
];
let state = test_state();
let result = simplify_constant_shape(nodes, &mut [], &state);
let slice = result.iter().find(|n| n.name == "slice").unwrap();
assert_eq!(slice.node_type, NodeType::Constant);
let vals = slice.inputs[0].value().unwrap().to_i64_vec().unwrap();
assert_eq!(vals, vec![10, 30, 50]);
}
#[test]
fn test_shape_slice_negative_index() {
let nodes = vec![
raw_node(
"shape",
NodeType::Shape,
vec![tensor_arg_with_shape("input", vec![2, 3, 4])],
vec![shape_arg("shape_out", 3)],
HashMap::new(),
),
raw_node(
"slice",
NodeType::Slice,
vec![
shape_arg("shape_out", 3),
const_i64_vec_arg("starts", &[-2]),
const_i64_vec_arg("ends", &[3]),
],
vec![shape_arg("slice_out", 2)],
HashMap::new(),
),
];
let state = test_state();
let result = simplify_constant_shape(nodes, &mut [], &state);
let slice = result.iter().find(|n| n.name == "slice").unwrap();
assert_eq!(slice.node_type, NodeType::Constant);
let vals = slice.inputs[0].value().unwrap().to_i64_vec().unwrap();
assert_eq!(vals, vec![3, 4]);
}
#[test]
fn test_slice_from_non_shape_not_replaced() {
let nodes = vec![
raw_node(
"relu",
NodeType::Relu,
vec![arg("input")],
vec![arg("relu_out")],
HashMap::new(),
),
raw_node(
"slice",
NodeType::Slice,
vec![
arg("relu_out"),
const_i64_vec_arg("starts", &[0]),
const_i64_vec_arg("ends", &[2]),
],
vec![arg("slice_out")],
HashMap::new(),
),
];
let state = test_state();
let result = simplify_constant_shape(nodes, &mut [], &state);
assert_eq!(result[1].node_type, NodeType::Slice);
}
#[test]
fn test_shape_slice_dynamic_starts_not_replaced() {
let nodes = vec![
raw_node(
"shape",
NodeType::Shape,
vec![tensor_arg_with_shape("input", vec![2, 3, 4])],
vec![shape_arg("shape_out", 3)],
HashMap::new(),
),
raw_node(
"slice",
NodeType::Slice,
vec![
shape_arg("shape_out", 3),
arg("dynamic_starts"), const_i64_vec_arg("ends", &[3]),
],
vec![shape_arg("slice_out", 2)],
HashMap::new(),
),
];
let state = test_state();
let result = simplify_constant_shape(nodes, &mut [], &state);
assert_eq!(result[1].node_type, NodeType::Slice);
}
#[test]
fn test_value_store_propagated_to_downstream_inputs() {
let nodes = vec![
raw_node(
"shape",
NodeType::Shape,
vec![tensor_arg_with_shape("input", vec![2, 3, 4])],
vec![shape_arg("shape_out", 3)],
HashMap::new(),
),
raw_node(
"gather",
NodeType::Gather,
vec![shape_arg("shape_out", 3), const_scalar_arg("idx", 1)],
vec![scalar_arg("dim_val", DType::I64)],
[("axis".to_string(), AttributeValue::Int64(0))]
.into_iter()
.collect(),
),
raw_node(
"add",
NodeType::Add,
vec![scalar_arg("dim_val", DType::I64), arg("other")],
vec![arg("add_out")],
HashMap::new(),
),
];
let state = test_state();
let result = simplify_constant_shape(nodes, &mut [], &state);
let add_node = result.iter().find(|n| n.name == "add").unwrap();
assert_eq!(add_node.inputs[0].value_source, ValueSource::Constant);
let val = add_node.inputs[0].value().unwrap().scalar_i64().unwrap();
assert_eq!(val, 3);
}
#[test]
fn test_value_store_propagated_to_graph_outputs() {
let nodes = vec![
raw_node(
"shape",
NodeType::Shape,
vec![tensor_arg_with_shape("input", vec![2, 3, 4])],
vec![shape_arg("shape_out", 3)],
HashMap::new(),
),
raw_node(
"gather",
NodeType::Gather,
vec![shape_arg("shape_out", 3), const_scalar_arg("idx", 0)],
vec![scalar_arg("dim_val", DType::I64)],
[("axis".to_string(), AttributeValue::Int64(0))]
.into_iter()
.collect(),
),
];
let mut graph_outputs = vec![scalar_arg("dim_val", DType::I64)];
let state = test_state();
simplify_constant_shape(nodes, &mut graph_outputs, &state);
assert_eq!(graph_outputs[0].value_source, ValueSource::Constant);
let val = graph_outputs[0].value().unwrap().scalar_i64().unwrap();
assert_eq!(val, 2);
}
}