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
148pub(super) fn push_debug_option_u8(out: &mut String, value: Option<u8>) {
149 match value {
150 Some(value) => {
151 out.push_str("Some(");
152 push_decimal_u8(out, value);
153 out.push(')');
154 }
155 None => out.push_str("None"),
156 }
157}
158
159pub(super) fn push_debug_option_workgroup(out: &mut String, value: Option<[u32; 3]>) {
160 match value {
161 Some([x, y, z]) => {
162 out.push_str("Some([");
163 push_decimal_u32(out, x);
164 out.push_str(", ");
165 push_decimal_u32(out, y);
166 out.push_str(", ");
167 push_decimal_u32(out, z);
168 out.push_str("])");
169 }
170 None => out.push_str("None"),
171 }
172}
173
174pub(super) fn push_decimal_u8(out: &mut String, value: u8) {
175 push_decimal_u32(out, u32::from(value));
176}
177
178pub(super) fn push_decimal_u32(out: &mut String, value: u32) {
179 let mut buf = [0_u8; 10];
180 let mut n = value;
181 let mut i = buf.len();
182 if n == 0 {
183 out.push('0');
184 return;
185 }
186 while n > 0 {
187 i -= 1;
188 buf[i] = b'0' + (n % 10) as u8;
189 n /= 10;
190 }
191 for &digit in &buf[i..] {
192 out.push(digit as char);
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::{
199 dispatch_policy_cache_digest, hex_encode, push_decimal_u32, push_lower_hex,
200 update_dispatch_policy_cache_hash,
201 };
202 use crate::backend::DispatchConfig;
203
204 #[test]
205 fn hex_encode_and_push_lower_hex_agree_on_known_bytes() {
206 assert_eq!(hex_encode(&[0x00, 0xff, 0x1a, 0x0f]), "00ff1a0f");
207 let mut out = String::from("k=");
208 push_lower_hex(&[0xde, 0xad, 0xbe, 0xef], &mut out);
209 assert_eq!(out, "k=deadbeef");
210 }
211
212 #[test]
213 fn push_decimal_u32_renders_boundaries() {
214 let mut out = String::new();
215 push_decimal_u32(&mut out, 0);
216 push_decimal_u32(&mut out, 42);
217 push_decimal_u32(&mut out, u32::MAX);
218 assert_eq!(out, "0424294967295");
219 }
220
221 #[test]
222 fn dispatch_policy_cache_digest_matches_shared_hasher_for_generated_configs() {
223 for case in 0..4096u32 {
224 let mut config = DispatchConfig::default();
225 if case & 1 != 0 {
226 config.ulp_budget = Some((case as u8).wrapping_mul(17).wrapping_add(1));
227 }
228 if case & 2 != 0 {
229 config.workgroup_override = Some([
230 1 + (case & 255),
231 1 + ((case.rotate_left(7) >> 3) & 31),
232 1 + ((case.rotate_right(5) >> 2) & 7),
233 ]);
234 }
235
236 let mut hasher = blake3::Hasher::new();
237 update_dispatch_policy_cache_hash(&mut hasher, &config);
238 assert_eq!(
239 dispatch_policy_cache_digest(&config),
240 *hasher.finalize().as_bytes(),
241 "Fix: dispatch-policy digest must stay single-sourced through update_dispatch_policy_cache_hash for generated case {case}."
242 );
243 }
244 }
245}