1use std::collections::hash_map::DefaultHasher;
54use std::fmt::Write as _;
55use std::hash::{Hash, Hasher};
56
57use crate::graph::{Graph, Node, NodeId};
58use crate::op::Op;
59use crate::shape::Shape;
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
66pub struct IgnoreConfig {
67 pub graph_name: bool,
69 pub node_names: bool,
71 pub origins: bool,
73}
74
75impl IgnoreConfig {
76 pub const EXACT: Self = Self {
79 graph_name: false,
80 node_names: false,
81 origins: false,
82 };
83
84 pub const SEMANTIC: Self = Self {
91 graph_name: true,
92 node_names: true,
93 origins: true,
94 };
95}
96
97impl Default for IgnoreConfig {
98 fn default() -> Self {
99 Self::SEMANTIC
100 }
101}
102
103struct HashWriter<'a, H: Hasher>(&'a mut H);
106
107impl<H: Hasher> std::fmt::Write for HashWriter<'_, H> {
108 fn write_str(&mut self, s: &str) -> std::fmt::Result {
109 self.0.write(s.as_bytes());
110 Ok(())
111 }
112}
113
114const SEP: u8 = 0xff;
117
118pub fn hash_op_into<H: Hasher>(op: &Op, cfg: IgnoreConfig, h: &mut H) {
120 let bodies = op.subgraphs();
121 if bodies.is_empty() {
122 let _ = write!(HashWriter(h), "{op:?}");
124 } else {
125 let mut surrogate = op.clone();
128 for body in surrogate.subgraphs_mut() {
129 *body = Graph::new("");
130 }
131 let _ = write!(HashWriter(h), "{surrogate:?}");
132 h.write_u8(SEP);
133 (bodies.len() as u64).hash(h);
134 for body in bodies {
135 hash_graph_into(body, cfg, h);
136 h.write_u8(SEP);
137 }
138 }
139 h.write_u8(SEP);
140}
141
142fn hash_node_into<H: Hasher>(node: &Node, cfg: IgnoreConfig, h: &mut H) {
144 node.id.0.hash(h);
145 hash_op_into(&node.op, cfg, h);
146 node.inputs.len().hash(h);
147 for input in &node.inputs {
148 input.0.hash(h);
149 }
150 node.shape.hash(h);
151 if !cfg.node_names {
152 node.name.hash(h);
153 h.write_u8(SEP);
154 }
155 if !cfg.origins {
156 let _ = write!(HashWriter(h), "{:?}", node.origin);
157 h.write_u8(SEP);
158 }
159}
160
161pub fn hash_graph_into<H: Hasher>(graph: &Graph, cfg: IgnoreConfig, h: &mut H) {
163 if !cfg.graph_name {
164 graph.name.hash(h);
165 h.write_u8(SEP);
166 }
167 graph.len().hash(h);
168 for node in graph.nodes() {
169 hash_node_into(node, cfg, h);
170 }
171 graph.outputs.len().hash(h);
172 for out in &graph.outputs {
173 out.0.hash(h);
174 }
175}
176
177pub fn structural_hash(graph: &Graph, cfg: IgnoreConfig) -> u64 {
184 let mut hasher = DefaultHasher::new();
185 hash_graph_into(graph, cfg, &mut hasher);
186 hasher.finish()
187}
188
189pub fn fingerprint(graph: &Graph) -> u64 {
192 structural_hash(graph, IgnoreConfig::SEMANTIC)
193}
194
195pub fn node_value_key(op: &Op, remapped_inputs: &[NodeId], shape: &Shape) -> u64 {
202 let mut hasher = DefaultHasher::new();
203 hash_op_into(op, IgnoreConfig::SEMANTIC, &mut hasher);
204 remapped_inputs.len().hash(&mut hasher);
205 for input in remapped_inputs {
206 input.0.hash(&mut hasher);
207 }
208 shape.hash(&mut hasher);
209 hasher.finish()
210}
211
212pub fn ops_deep_eq(a: &Op, b: &Op, cfg: IgnoreConfig) -> bool {
217 if a.kind() != b.kind() {
220 return false;
221 }
222
223 let (a_bodies, b_bodies) = (a.subgraphs(), b.subgraphs());
224 if a_bodies.len() != b_bodies.len() {
225 return false;
226 }
227
228 if a_bodies.is_empty() {
229 return format!("{a:?}") == format!("{b:?}");
230 }
231
232 let blank = |op: &Op| {
233 let mut surrogate = op.clone();
234 for body in surrogate.subgraphs_mut() {
235 *body = Graph::new("");
236 }
237 format!("{surrogate:?}")
238 };
239 if blank(a) != blank(b) {
240 return false;
241 }
242 a_bodies
243 .iter()
244 .zip(b_bodies.iter())
245 .all(|(x, y)| structurally_eq(x, y, cfg))
246}
247
248pub fn structurally_eq(a: &Graph, b: &Graph, cfg: IgnoreConfig) -> bool {
253 if !cfg.graph_name && a.name != b.name {
254 return false;
255 }
256 if a.len() != b.len() || a.outputs != b.outputs {
257 return false;
258 }
259
260 for (x, y) in a.nodes().iter().zip(b.nodes().iter()) {
261 if x.id != y.id || x.inputs != y.inputs || x.shape != y.shape {
262 return false;
263 }
264 if !cfg.node_names && x.name != y.name {
265 return false;
266 }
267 if !cfg.origins && format!("{:?}", x.origin) != format!("{:?}", y.origin) {
268 return false;
269 }
270 if !ops_deep_eq(&x.op, &y.op, cfg) {
271 return false;
272 }
273 }
274 true
275}
276
277impl Graph {
278 pub fn fingerprint(&self) -> u64 {
281 fingerprint(self)
282 }
283
284 pub fn structurally_eq(&self, other: &Graph, cfg: IgnoreConfig) -> bool {
289 structurally_eq(self, other, cfg)
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use crate::op::{Activation, BinaryOp};
297 use crate::{DType, Shape};
298
299 fn two_layer(name: &str) -> Graph {
300 let mut g = Graph::new(name);
301 let x = g.input("x", Shape::new(&[2, 4], DType::F32));
302 let w = g.param("w", Shape::new(&[4, 4], DType::F32));
303 let mm = g.matmul(x, w, Shape::new(&[2, 4], DType::F32));
304 let act = g.add_node(
305 Op::Activation(Activation::Gelu),
306 vec![mm],
307 Shape::new(&[2, 4], DType::F32),
308 );
309 g.set_outputs(vec![act]);
310 g
311 }
312
313 #[test]
314 fn identical_graphs_agree() {
315 let a = two_layer("m");
316 let b = two_layer("m");
317 assert_eq!(a.fingerprint(), b.fingerprint());
318 assert!(a.structurally_eq(&b, IgnoreConfig::EXACT));
319 }
320
321 #[test]
322 fn op_payload_change_is_detected() {
323 let a = two_layer("m");
324 let mut b = two_layer("m");
325 b.node_mut(NodeId(3)).op = Op::Activation(Activation::Relu);
327 assert_ne!(a.fingerprint(), b.fingerprint());
328 assert!(!a.structurally_eq(&b, IgnoreConfig::SEMANTIC));
329 }
330
331 #[test]
332 fn rewiring_is_detected() {
333 let a = two_layer("m");
334 let mut b = two_layer("m");
335 b.set_inputs(NodeId(3), vec![NodeId(0)]);
336 assert_ne!(a.fingerprint(), b.fingerprint());
337 assert!(!a.structurally_eq(&b, IgnoreConfig::SEMANTIC));
338 }
339
340 #[test]
341 fn semantic_config_ignores_labels_exact_does_not() {
342 let a = two_layer("m");
343 let mut b = two_layer("other_name");
344 b.node_mut(NodeId(3)).name = Some("relabelled".into());
345
346 assert_eq!(
347 structural_hash(&a, IgnoreConfig::SEMANTIC),
348 structural_hash(&b, IgnoreConfig::SEMANTIC)
349 );
350 assert!(a.structurally_eq(&b, IgnoreConfig::SEMANTIC));
351
352 assert_ne!(
353 structural_hash(&a, IgnoreConfig::EXACT),
354 structural_hash(&b, IgnoreConfig::EXACT)
355 );
356 assert!(!a.structurally_eq(&b, IgnoreConfig::EXACT));
357 }
358
359 #[test]
360 fn shallow_partial_eq_is_the_gap_this_module_closes() {
361 let a = two_layer("m");
364 let mut b = two_layer("m");
365 b.node_mut(NodeId(3)).op = Op::Binary(BinaryOp::Add);
366 b.set_inputs(NodeId(3), vec![NodeId(2), NodeId(2)]);
367
368 assert_eq!(a, b, "Graph::eq is documented-shallow");
369 assert!(!a.structurally_eq(&b, IgnoreConfig::SEMANTIC));
370 assert_ne!(a.fingerprint(), b.fingerprint());
371 }
372
373 #[test]
374 fn nested_bodies_participate() {
375 let scan = |act: Activation| {
376 let mut body = Graph::new("body");
377 let c = body.input("carry", Shape::new(&[4], DType::F32));
378 let y = body.add_node(Op::Activation(act), vec![c], Shape::new(&[4], DType::F32));
379 body.set_outputs(vec![y]);
380
381 let mut g = Graph::new("outer");
382 let init = g.input("init", Shape::new(&[4], DType::F32));
383 let s = g.add_node(
384 Op::Scan {
385 body: Box::new(body),
386 length: 8,
387 save_trajectory: false,
388 num_bcast: 0,
389 num_xs: 0,
390 num_checkpoints: 0,
391 },
392 vec![init],
393 Shape::new(&[4], DType::F32),
394 );
395 g.set_outputs(vec![s]);
396 g
397 };
398
399 let a = scan(Activation::Gelu);
400 let b = scan(Activation::Gelu);
401 let c = scan(Activation::Relu);
402
403 assert_eq!(a.fingerprint(), b.fingerprint());
404 assert!(a.structurally_eq(&b, IgnoreConfig::SEMANTIC));
405
406 assert_ne!(a.fingerprint(), c.fingerprint());
408 assert!(!a.structurally_eq(&c, IgnoreConfig::SEMANTIC));
409 }
410
411 #[test]
412 fn ignore_flags_reach_into_nested_bodies() {
413 let build = |body_node_name: &str| {
416 let mut body = Graph::new("body");
417 let c = body.input("carry", Shape::new(&[4], DType::F32));
418 let y = body.add_node(
419 Op::Activation(Activation::Gelu),
420 vec![c],
421 Shape::new(&[4], DType::F32),
422 );
423 body.node_mut(y).name = Some(body_node_name.to_string());
424 body.set_outputs(vec![y]);
425
426 let mut g = Graph::new("outer");
427 let init = g.input("init", Shape::new(&[4], DType::F32));
428 let s = g.add_node(
429 Op::Scan {
430 body: Box::new(body),
431 length: 8,
432 save_trajectory: false,
433 num_bcast: 0,
434 num_xs: 0,
435 num_checkpoints: 0,
436 },
437 vec![init],
438 Shape::new(&[4], DType::F32),
439 );
440 g.set_outputs(vec![s]);
441 g
442 };
443
444 let a = build("before");
445 let b = build("after");
446 assert_eq!(a.fingerprint(), b.fingerprint());
447 assert!(a.structurally_eq(&b, IgnoreConfig::SEMANTIC));
448 assert!(!a.structurally_eq(&b, IgnoreConfig::EXACT));
449 }
450
451 #[test]
452 fn value_key_separates_operand_order() {
453 let shape = Shape::new(&[4], DType::F32);
454 let op = Op::Binary(BinaryOp::Sub);
455 let ab = node_value_key(&op, &[NodeId(1), NodeId(2)], &shape);
456 let ba = node_value_key(&op, &[NodeId(2), NodeId(1)], &shape);
457 assert_ne!(ab, ba, "Sub is not commutative — order must key distinctly");
458 }
459}