vyre_driver/pipeline/
hashing.rs1use crate::backend::DispatchConfig;
4use vyre_foundation::ir::Program;
5use vyre_spec::BackendId;
6
7pub fn try_normalized_program_cache_digest(program: &Program) -> Result<[u8; 32], String> {
20 program.try_normalized_cache_digest()
21}
22
23#[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
29pub 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#[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#[must_use]
69pub fn dispatch_policy_cache_string(config: &DispatchConfig) -> String {
70 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#[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
89pub 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#[must_use]
101pub fn hex_short(bytes: &[u8; 32]) -> String {
102 hex_encode(&bytes[..8])
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107pub struct PipelineDeviceFingerprint {
108 pub vendor: u32,
110 pub device: u32,
112 pub driver_digest: [u8; 32],
114}
115
116impl PipelineDeviceFingerprint {
117 #[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 #[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}