use std::collections::hash_map::DefaultHasher;
use std::fmt::Write as _;
use std::hash::{Hash, Hasher};
use crate::graph::{Graph, Node, NodeId};
use crate::op::Op;
use crate::shape::Shape;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct IgnoreConfig {
pub graph_name: bool,
pub node_names: bool,
pub origins: bool,
}
impl IgnoreConfig {
pub const EXACT: Self = Self {
graph_name: false,
node_names: false,
origins: false,
};
pub const SEMANTIC: Self = Self {
graph_name: true,
node_names: true,
origins: true,
};
}
impl Default for IgnoreConfig {
fn default() -> Self {
Self::SEMANTIC
}
}
struct HashWriter<'a, H: Hasher>(&'a mut H);
impl<H: Hasher> std::fmt::Write for HashWriter<'_, H> {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
self.0.write(s.as_bytes());
Ok(())
}
}
const SEP: u8 = 0xff;
pub fn hash_op_into<H: Hasher>(op: &Op, cfg: IgnoreConfig, h: &mut H) {
let bodies = op.subgraphs();
if bodies.is_empty() {
let _ = write!(HashWriter(h), "{op:?}");
} else {
let mut surrogate = op.clone();
for body in surrogate.subgraphs_mut() {
*body = Graph::new("");
}
let _ = write!(HashWriter(h), "{surrogate:?}");
h.write_u8(SEP);
(bodies.len() as u64).hash(h);
for body in bodies {
hash_graph_into(body, cfg, h);
h.write_u8(SEP);
}
}
h.write_u8(SEP);
}
fn hash_node_into<H: Hasher>(node: &Node, cfg: IgnoreConfig, h: &mut H) {
node.id.0.hash(h);
hash_op_into(&node.op, cfg, h);
node.inputs.len().hash(h);
for input in &node.inputs {
input.0.hash(h);
}
node.shape.hash(h);
if !cfg.node_names {
node.name.hash(h);
h.write_u8(SEP);
}
if !cfg.origins {
let _ = write!(HashWriter(h), "{:?}", node.origin);
h.write_u8(SEP);
}
}
pub fn hash_graph_into<H: Hasher>(graph: &Graph, cfg: IgnoreConfig, h: &mut H) {
if !cfg.graph_name {
graph.name.hash(h);
h.write_u8(SEP);
}
graph.len().hash(h);
for node in graph.nodes() {
hash_node_into(node, cfg, h);
}
graph.outputs.len().hash(h);
for out in &graph.outputs {
out.0.hash(h);
}
}
pub fn structural_hash(graph: &Graph, cfg: IgnoreConfig) -> u64 {
let mut hasher = DefaultHasher::new();
hash_graph_into(graph, cfg, &mut hasher);
hasher.finish()
}
pub fn fingerprint(graph: &Graph) -> u64 {
structural_hash(graph, IgnoreConfig::SEMANTIC)
}
pub fn node_value_key(op: &Op, remapped_inputs: &[NodeId], shape: &Shape) -> u64 {
let mut hasher = DefaultHasher::new();
hash_op_into(op, IgnoreConfig::SEMANTIC, &mut hasher);
remapped_inputs.len().hash(&mut hasher);
for input in remapped_inputs {
input.0.hash(&mut hasher);
}
shape.hash(&mut hasher);
hasher.finish()
}
pub fn ops_deep_eq(a: &Op, b: &Op, cfg: IgnoreConfig) -> bool {
if a.kind() != b.kind() {
return false;
}
let (a_bodies, b_bodies) = (a.subgraphs(), b.subgraphs());
if a_bodies.len() != b_bodies.len() {
return false;
}
if a_bodies.is_empty() {
return format!("{a:?}") == format!("{b:?}");
}
let blank = |op: &Op| {
let mut surrogate = op.clone();
for body in surrogate.subgraphs_mut() {
*body = Graph::new("");
}
format!("{surrogate:?}")
};
if blank(a) != blank(b) {
return false;
}
a_bodies
.iter()
.zip(b_bodies.iter())
.all(|(x, y)| structurally_eq(x, y, cfg))
}
pub fn structurally_eq(a: &Graph, b: &Graph, cfg: IgnoreConfig) -> bool {
if !cfg.graph_name && a.name != b.name {
return false;
}
if a.len() != b.len() || a.outputs != b.outputs {
return false;
}
for (x, y) in a.nodes().iter().zip(b.nodes().iter()) {
if x.id != y.id || x.inputs != y.inputs || x.shape != y.shape {
return false;
}
if !cfg.node_names && x.name != y.name {
return false;
}
if !cfg.origins && format!("{:?}", x.origin) != format!("{:?}", y.origin) {
return false;
}
if !ops_deep_eq(&x.op, &y.op, cfg) {
return false;
}
}
true
}
impl Graph {
pub fn fingerprint(&self) -> u64 {
fingerprint(self)
}
pub fn structurally_eq(&self, other: &Graph, cfg: IgnoreConfig) -> bool {
structurally_eq(self, other, cfg)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::op::{Activation, BinaryOp};
use crate::{DType, Shape};
fn two_layer(name: &str) -> Graph {
let mut g = Graph::new(name);
let x = g.input("x", Shape::new(&[2, 4], DType::F32));
let w = g.param("w", Shape::new(&[4, 4], DType::F32));
let mm = g.matmul(x, w, Shape::new(&[2, 4], DType::F32));
let act = g.add_node(
Op::Activation(Activation::Gelu),
vec![mm],
Shape::new(&[2, 4], DType::F32),
);
g.set_outputs(vec![act]);
g
}
#[test]
fn identical_graphs_agree() {
let a = two_layer("m");
let b = two_layer("m");
assert_eq!(a.fingerprint(), b.fingerprint());
assert!(a.structurally_eq(&b, IgnoreConfig::EXACT));
}
#[test]
fn op_payload_change_is_detected() {
let a = two_layer("m");
let mut b = two_layer("m");
b.node_mut(NodeId(3)).op = Op::Activation(Activation::Relu);
assert_ne!(a.fingerprint(), b.fingerprint());
assert!(!a.structurally_eq(&b, IgnoreConfig::SEMANTIC));
}
#[test]
fn rewiring_is_detected() {
let a = two_layer("m");
let mut b = two_layer("m");
b.set_inputs(NodeId(3), vec![NodeId(0)]);
assert_ne!(a.fingerprint(), b.fingerprint());
assert!(!a.structurally_eq(&b, IgnoreConfig::SEMANTIC));
}
#[test]
fn semantic_config_ignores_labels_exact_does_not() {
let a = two_layer("m");
let mut b = two_layer("other_name");
b.node_mut(NodeId(3)).name = Some("relabelled".into());
assert_eq!(
structural_hash(&a, IgnoreConfig::SEMANTIC),
structural_hash(&b, IgnoreConfig::SEMANTIC)
);
assert!(a.structurally_eq(&b, IgnoreConfig::SEMANTIC));
assert_ne!(
structural_hash(&a, IgnoreConfig::EXACT),
structural_hash(&b, IgnoreConfig::EXACT)
);
assert!(!a.structurally_eq(&b, IgnoreConfig::EXACT));
}
#[test]
fn shallow_partial_eq_is_the_gap_this_module_closes() {
let a = two_layer("m");
let mut b = two_layer("m");
b.node_mut(NodeId(3)).op = Op::Binary(BinaryOp::Add);
b.set_inputs(NodeId(3), vec![NodeId(2), NodeId(2)]);
assert_eq!(a, b, "Graph::eq is documented-shallow");
assert!(!a.structurally_eq(&b, IgnoreConfig::SEMANTIC));
assert_ne!(a.fingerprint(), b.fingerprint());
}
#[test]
fn nested_bodies_participate() {
let scan = |act: Activation| {
let mut body = Graph::new("body");
let c = body.input("carry", Shape::new(&[4], DType::F32));
let y = body.add_node(Op::Activation(act), vec![c], Shape::new(&[4], DType::F32));
body.set_outputs(vec![y]);
let mut g = Graph::new("outer");
let init = g.input("init", Shape::new(&[4], DType::F32));
let s = g.add_node(
Op::Scan {
body: Box::new(body),
length: 8,
save_trajectory: false,
num_bcast: 0,
num_xs: 0,
num_checkpoints: 0,
},
vec![init],
Shape::new(&[4], DType::F32),
);
g.set_outputs(vec![s]);
g
};
let a = scan(Activation::Gelu);
let b = scan(Activation::Gelu);
let c = scan(Activation::Relu);
assert_eq!(a.fingerprint(), b.fingerprint());
assert!(a.structurally_eq(&b, IgnoreConfig::SEMANTIC));
assert_ne!(a.fingerprint(), c.fingerprint());
assert!(!a.structurally_eq(&c, IgnoreConfig::SEMANTIC));
}
#[test]
fn ignore_flags_reach_into_nested_bodies() {
let build = |body_node_name: &str| {
let mut body = Graph::new("body");
let c = body.input("carry", Shape::new(&[4], DType::F32));
let y = body.add_node(
Op::Activation(Activation::Gelu),
vec![c],
Shape::new(&[4], DType::F32),
);
body.node_mut(y).name = Some(body_node_name.to_string());
body.set_outputs(vec![y]);
let mut g = Graph::new("outer");
let init = g.input("init", Shape::new(&[4], DType::F32));
let s = g.add_node(
Op::Scan {
body: Box::new(body),
length: 8,
save_trajectory: false,
num_bcast: 0,
num_xs: 0,
num_checkpoints: 0,
},
vec![init],
Shape::new(&[4], DType::F32),
);
g.set_outputs(vec![s]);
g
};
let a = build("before");
let b = build("after");
assert_eq!(a.fingerprint(), b.fingerprint());
assert!(a.structurally_eq(&b, IgnoreConfig::SEMANTIC));
assert!(!a.structurally_eq(&b, IgnoreConfig::EXACT));
}
#[test]
fn value_key_separates_operand_order() {
let shape = Shape::new(&[4], DType::F32);
let op = Op::Binary(BinaryOp::Sub);
let ab = node_value_key(&op, &[NodeId(1), NodeId(2)], &shape);
let ba = node_value_key(&op, &[NodeId(2), NodeId(1)], &shape);
assert_ne!(ab, ba, "Sub is not commutative — order must key distinctly");
}
}