Skip to main content

vyre_driver/pipeline/
hashing.rs

1//! Stable hashing helpers for compiled-pipeline cache identity.
2
3use crate::backend::DispatchConfig;
4use vyre_foundation::ir::Program;
5use vyre_spec::BackendId;
6
7/// Return the normalized program digest used by backend pipeline caches.
8///
9/// Thin forwarder: the digest and its per-`Program` memo are owned by
10/// [`vyre_foundation::ir::Program::try_normalized_cache_digest`], so the
11/// algorithm has exactly one implementation and is computed at most once per
12/// program value instead of once per dispatch.
13///
14/// # Errors
15///
16/// Returns when the program contains an IR type or node shape that cannot be
17/// serialized into stable cache identity. Dispatch admission should surface the
18/// error rather than panic or generate a lossy cache key.
19pub fn try_normalized_program_cache_digest(program: &Program) -> Result<[u8; 32], String> {
20    program.try_normalized_cache_digest()
21}
22
23/// Return the normalized program digest used by backend pipeline caches.
24#[must_use]
25pub fn normalized_program_cache_digest(program: &Program) -> [u8; 32] {
26    try_normalized_program_cache_digest(program).unwrap_or([0u8; 32])
27}
28
29/// Append dispatch policy fields that alter generated backend code to a cache
30/// hasher.
31pub fn update_dispatch_policy_cache_hash(hasher: &mut blake3::Hasher, config: &DispatchConfig) {
32    hasher.update(b"ulp\0");
33    match config.ulp_budget {
34        Some(ulp) => {
35            hasher.update(&[1, ulp]);
36        }
37        None => {
38            hasher.update(&[0, 0]);
39        }
40    };
41    hasher.update(b"\0wg\0");
42    match config.workgroup_override {
43        Some(workgroup) => {
44            hasher.update(&[1]);
45            for axis in workgroup {
46                hasher.update(&axis.to_le_bytes());
47            }
48        }
49        None => {
50            hasher.update(&[0]);
51        }
52    };
53}
54
55/// Return the dispatch-policy digest used inside backend cache keys.
56///
57/// This keeps policy serialization single-sourced while letting backend cache
58/// identities use the shared tuple-boundary-preserving key envelope instead of
59/// owning a second ad hoc hasher sequence.
60#[must_use]
61pub fn dispatch_policy_cache_digest(config: &DispatchConfig) -> [u8; 32] {
62    let mut hasher = blake3::Hasher::new();
63    update_dispatch_policy_cache_hash(&mut hasher, config);
64    *hasher.finalize().as_bytes()
65}
66
67/// Human-readable dispatch policy fingerprint for cache metadata.
68#[must_use]
69pub fn dispatch_policy_cache_string(config: &DispatchConfig) -> String {
70    // "ulp=" (4) + max u8 decimal (3) + ":wg=" (4) + workgroup repr
71    // (~32) ≈ 64 bytes worst case; pre-size so the 4 push_str calls
72    // do not realloc.
73    let mut policy = String::with_capacity(64);
74    policy.push_str("ulp=");
75    push_debug_option_u8(&mut policy, config.ulp_budget);
76    policy.push_str(":wg=");
77    push_debug_option_workgroup(&mut policy, config.workgroup_override);
78    policy
79}
80
81/// Hex-encode bytes using lowercase ASCII.
82#[must_use]
83pub fn hex_encode(bytes: &[u8]) -> String {
84    let mut out = String::with_capacity(bytes.len() * 2);
85    push_lower_hex(bytes, &mut out);
86    out
87}
88
89/// Append the lowercase-hex encoding of `bytes` to `out`. Single owner of the
90/// lowercase-hex nibble loop for the whole driver.
91pub fn push_lower_hex(bytes: &[u8], out: &mut String) {
92    const HEX: &[u8; 16] = b"0123456789abcdef";
93    for &byte in bytes {
94        out.push(HEX[(byte >> 4) as usize] as char);
95        out.push(HEX[(byte & 0x0f) as usize] as char);
96    }
97}
98
99/// Hex-encode the first eight bytes of a 32-byte digest for compact ids.
100#[must_use]
101pub fn hex_short(bytes: &[u8; 32]) -> String {
102    hex_encode(&bytes[..8])
103}
104
105/// Stable device fingerprint for persistent pipeline caches.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107pub struct PipelineDeviceFingerprint {
108    /// Vendor identifier.
109    pub vendor: u32,
110    /// Device identifier.
111    pub device: u32,
112    /// Cryptographic digest of driver/runtime revision text.
113    pub driver_digest: [u8; 32],
114}
115
116impl PipelineDeviceFingerprint {
117    /// Build a fingerprint from numeric identifiers and revision text.
118    #[must_use]
119    pub fn from_parts(vendor: u32, device: u32, revision: &str, revision_extra: &str) -> Self {
120        let mut hasher = blake3::Hasher::new();
121        hasher.update(b"vyre-pipeline-device-fingerprint-v1\0");
122        hasher.update(revision.as_bytes());
123        hasher.update(b"\0extra\0");
124        hasher.update(revision_extra.as_bytes());
125        Self {
126            vendor,
127            device,
128            driver_digest: *hasher.finalize().as_bytes(),
129        }
130    }
131
132    /// Compose a cache key from canonical program digest and device identity.
133    #[must_use]
134    pub fn cache_key(self, program_digest: [u8; 32]) -> [u8; 32] {
135        let mut hasher = blake3::Hasher::new();
136        hasher.update(b"vyre-disk-pipeline-cache-key-v1\0program\0");
137        hasher.update(&program_digest);
138        hasher.update(b"\0vendor\0");
139        hasher.update(&self.vendor.to_le_bytes());
140        hasher.update(b"\0device\0");
141        hasher.update(&self.device.to_le_bytes());
142        hasher.update(b"\0driver\0");
143        hasher.update(&self.driver_digest);
144        *hasher.finalize().as_bytes()
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::{
151        dispatch_policy_cache_digest, hex_encode, push_decimal_u32, push_lower_hex,
152        update_dispatch_policy_cache_hash,
153    };
154    use crate::backend::DispatchConfig;
155
156    #[test]
157    fn hex_encode_and_push_lower_hex_agree_on_known_bytes() {
158        assert_eq!(hex_encode(&[0x00, 0xff, 0x1a, 0x0f]), "00ff1a0f");
159        let mut out = String::from("k=");
160        push_lower_hex(&[0xde, 0xad, 0xbe, 0xef], &mut out);
161        assert_eq!(out, "k=deadbeef");
162    }
163
164    #[test]
165    fn push_decimal_u32_renders_boundaries() {
166        let mut out = String::new();
167        push_decimal_u32(&mut out, 0);
168        push_decimal_u32(&mut out, 42);
169        push_decimal_u32(&mut out, u32::MAX);
170        assert_eq!(out, "0424294967295");
171    }
172
173    #[test]
174    fn dispatch_policy_cache_digest_matches_shared_hasher_for_generated_configs() {
175        for case in 0..4096u32 {
176            let mut config = DispatchConfig::default();
177            if case & 1 != 0 {
178                config.ulp_budget = Some((case as u8).wrapping_mul(17).wrapping_add(1));
179            }
180            if case & 2 != 0 {
181                config.workgroup_override = Some([
182                    1 + (case & 255),
183                    1 + ((case.rotate_left(7) >> 3) & 31),
184                    1 + ((case.rotate_right(5) >> 2) & 7),
185                ]);
186            }
187
188            let mut hasher = blake3::Hasher::new();
189            update_dispatch_policy_cache_hash(&mut hasher, &config);
190            assert_eq!(
191                dispatch_policy_cache_digest(&config),
192                *hasher.finalize().as_bytes(),
193                "Fix: dispatch-policy digest must stay single-sourced through update_dispatch_policy_cache_hash for generated case {case}."
194            );
195        }
196    }
197}
198
199pub(super) fn push_debug_option_u8(out: &mut String, value: Option<u8>) {
200    match value {
201        Some(value) => {
202            out.push_str("Some(");
203            push_decimal_u8(out, value);
204            out.push(')');
205        }
206        None => out.push_str("None"),
207    }
208}
209
210pub(super) fn push_debug_option_workgroup(out: &mut String, value: Option<[u32; 3]>) {
211    match value {
212        Some([x, y, z]) => {
213            out.push_str("Some([");
214            push_decimal_u32(out, x);
215            out.push_str(", ");
216            push_decimal_u32(out, y);
217            out.push_str(", ");
218            push_decimal_u32(out, z);
219            out.push_str("])");
220        }
221        None => out.push_str("None"),
222    }
223}
224
225pub(super) fn push_decimal_u8(out: &mut String, value: u8) {
226    push_decimal_u32(out, u32::from(value));
227}
228
229pub(super) fn push_decimal_u32(out: &mut String, value: u32) {
230    let mut buf = [0_u8; 10];
231    let mut n = value;
232    let mut i = buf.len();
233    if n == 0 {
234        out.push('0');
235        return;
236    }
237    while n > 0 {
238        i -= 1;
239        buf[i] = b'0' + (n % 10) as u8;
240        n /= 10;
241    }
242    for &digit in &buf[i..] {
243        out.push(digit as char);
244    }
245}