Skip to main content

vyre_runtime/pipeline_cache/
fingerprint.rs

1//! [`PipelineFingerprint`]  -  the content-addressed key for cache
2//! lookups. Wrapped in its own module so the field-allowlist invariant
3//! and hashing helper sit next to the public type.
4
5use vyre_foundation::ir::Program;
6
7/// Program-intrinsic fields that are permitted to contribute to
8/// [`PipelineFingerprint`].
9///
10/// The key is intentionally narrow:
11/// - canonical IR node graph
12/// - declared buffer layout (names, bindings, access, dtypes, counts)
13/// - the `Program`'s declared workgroup size
14/// - canonical wire-format framing emitted by `Program::to_wire()`
15///
16/// The key intentionally excludes every dispatch-time concern:
17/// - input buffer count or byte contents
18/// - `DispatchConfig` labels, profiles, timeout, and ULP budget
19/// - runtime workgroup overrides or launch geometry
20///
21/// The compile-time assertion below pins `PipelineFingerprint::of` to
22/// `fn(&Program) -> PipelineFingerprint`, so no per-dispatch structure can
23/// accidentally enter the key without changing the public signature.
24const PIPELINE_FINGERPRINT_ALLOWED_FIELDS: &[&str] = &[
25    "canonical_ir_graph",
26    "buffer_layout",
27    "declared_workgroup_size",
28    "canonical_wire_framing",
29];
30
31/// The blake3 fingerprint of a canonicalized Program. 32 bytes so
32/// collisions are cryptographically impossible for our scale.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub struct PipelineFingerprint(pub [u8; 32]);
35
36const _: fn(&Program) -> PipelineFingerprint = PipelineFingerprint::of;
37
38impl PipelineFingerprint {
39    /// Derive a fingerprint from a Program. Runs
40    /// `vyre_foundation::optimizer::passes::algebraic::canonicalize_engine::run`
41    /// first so semantically-equal Programs share a fingerprint.
42    ///
43    /// Only program-intrinsic state is allowed into this hash. The
44    /// fingerprint must stay stable across different dispatch inputs and
45    /// execution-time knobs so the cache remains content-addressed rather
46    /// than dispatch-addressed.
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// use vyre_foundation::ir::Program;
52    /// use vyre_runtime::PipelineFingerprint;
53    ///
54    /// let a = Program::empty();
55    /// let b = Program::empty();
56    ///
57    /// assert_eq!(PipelineFingerprint::of(&a), PipelineFingerprint::of(&b));
58    /// ```
59    #[must_use]
60    pub fn of(program: &Program) -> Self {
61        Self(hash_pipeline_fingerprint(program))
62    }
63
64    /// Hex-encode the fingerprint for human display + path-safe
65    /// storage. Lowercase, no separators, 64 chars.
66    #[must_use]
67    pub fn hex(&self) -> String {
68        let mut out = String::with_capacity(64);
69        self.push_hex(&mut out);
70        out
71    }
72
73    pub(super) fn push_hex(&self, out: &mut String) {
74        const HEX: &[u8; 16] = b"0123456789abcdef";
75        for &byte in &self.0 {
76            out.push(HEX[(byte >> 4) as usize] as char);
77            out.push(HEX[(byte & 0x0f) as usize] as char);
78        }
79    }
80}
81
82fn hash_pipeline_fingerprint(program: &Program) -> [u8; 32] {
83    debug_assert_eq!(
84        PIPELINE_FINGERPRINT_ALLOWED_FIELDS.len(),
85        4,
86        "Fix: update PIPELINE_FINGERPRINT_ALLOWED_FIELDS whenever the fingerprint contract changes."
87    );
88    // Audit P0 #26: routes through the shared
89    // `vyre_foundation::optimizer::pipeline_fingerprint_bytes` so
90    // AOT-emitted artifacts and runtime-cache blobs cannot drift apart.
91    vyre_foundation::optimizer::pipeline_fingerprint_bytes(program)
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::pipeline_cache::test_helpers::tiny_program;
98    use vyre_foundation::ir::{BufferDecl, DataType, Expr, Node};
99
100    #[test]
101    fn fingerprint_is_deterministic() {
102        let a = PipelineFingerprint::of(&tiny_program());
103        let b = PipelineFingerprint::of(&tiny_program());
104        assert_eq!(a, b);
105    }
106
107    #[test]
108    fn fingerprint_hex_is_64_chars() {
109        let fp = PipelineFingerprint::of(&tiny_program());
110        assert_eq!(fp.hex().len(), 64);
111    }
112
113    #[test]
114    fn canonically_equal_programs_share_fingerprint() {
115        // `a + 1` and `1 + a` canonicalize to the same IR → same fingerprint.
116        let p1 = Program::wrapped(
117            vec![BufferDecl::read_write("out", 0, DataType::U32).with_count(1)],
118            [1, 1, 1],
119            vec![Node::store(
120                "out",
121                Expr::u32(0),
122                Expr::add(Expr::var("a"), Expr::u32(1)),
123            )],
124        );
125        let p2 = Program::wrapped(
126            vec![BufferDecl::read_write("out", 0, DataType::U32).with_count(1)],
127            [1, 1, 1],
128            vec![Node::store(
129                "out",
130                Expr::u32(0),
131                Expr::add(Expr::u32(1), Expr::var("a")),
132            )],
133        );
134        let fp1 = PipelineFingerprint::of(&p1);
135        let fp2 = PipelineFingerprint::of(&p2);
136        assert_eq!(
137            fp1, fp2,
138            "canonicalize makes `a+1` and `1+a` share a fingerprint"
139        );
140    }
141
142    #[test]
143    fn fingerprint_changes_when_declared_program_shape_changes() {
144        let base = tiny_program();
145        let widened = Program::wrapped(
146            vec![BufferDecl::read_write("out", 0, DataType::U32).with_count(1)],
147            [64, 1, 1],
148            vec![Node::store("out", Expr::u32(0), Expr::u32(42))],
149        );
150
151        assert_ne!(
152            PipelineFingerprint::of(&base),
153            PipelineFingerprint::of(&widened),
154            "declared workgroup size is program-intrinsic and must change the fingerprint"
155        );
156    }
157}