1use std::hash::{Hash, Hasher as _};
2use std::sync::atomic::Ordering;
3use std::sync::Arc;
4
5use rustc_hash::FxHasher;
6use vyre_spec::bin_op::OpIntensity;
7
8use crate::ir::{Expr, Node};
9use crate::ir_inner::model::expr::Ident;
10use crate::ir_inner::model::spec_types::BufferAccess;
11use crate::transform::visit::{walk_nodes_and_exprs, ExprVisitor, NodeVisitor};
12
13use super::Program;
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17#[repr(u8)]
18pub enum ProgramMutationProvenance {
19 Clean = 0,
21 NonComposableFlag = 1,
23 WorkgroupSize = 2,
25 ParallelRegionSize = 3,
27 EntryMutation = 4,
29 InternalShapeMutation = 5,
31 Unknown = 255,
33}
34
35impl ProgramMutationProvenance {
36 #[inline]
37 const fn from_code(code: u8) -> Self {
38 match code {
39 0 => Self::Clean,
40 1 => Self::NonComposableFlag,
41 2 => Self::WorkgroupSize,
42 3 => Self::ParallelRegionSize,
43 4 => Self::EntryMutation,
44 5 => Self::InternalShapeMutation,
45 _ => Self::Unknown,
46 }
47 }
48}
49
50fn mix_wire_fallback_hashable<T: Hash>(hasher: &mut blake3::Hasher, value: &T) {
51 let mut state = FxHasher::default();
52 value.hash(&mut state);
53 hasher.update(&state.finish().to_le_bytes());
54}
55
56struct FallbackWireHasher<'a>(&'a mut blake3::Hasher);
58
59impl NodeVisitor for FallbackWireHasher<'_> {
60 fn visit_node(&mut self, node: &Node) {
61 let h = &mut *self.0;
62 match node {
63 Node::Let { name, .. } => {
64 h.update(b"n:Let\0");
65 h.update(name.as_bytes());
66 }
67 Node::Assign { name, .. } => {
68 h.update(b"n:Assign\0");
69 h.update(name.as_bytes());
70 }
71 Node::Store { buffer, .. } => {
72 h.update(b"n:Store\0");
73 h.update(buffer.as_bytes());
74 }
75 Node::If { .. } => {
76 h.update(b"n:If\0");
77 }
78 Node::Loop { var, .. } => {
79 h.update(b"n:Loop\0");
80 h.update(var.as_bytes());
81 }
82 Node::IndirectDispatch {
83 count_buffer,
84 count_offset,
85 } => {
86 h.update(b"n:IndirectDispatch\0");
87 h.update(count_buffer.as_bytes());
88 h.update(&count_offset.to_le_bytes());
89 }
90 Node::AsyncLoad {
91 source,
92 destination,
93 tag,
94 ..
95 } => {
96 h.update(b"n:AsyncLoad\0");
97 h.update(source.as_bytes());
98 h.update(destination.as_bytes());
99 h.update(tag.as_bytes());
100 }
101 Node::AsyncStore {
102 source,
103 destination,
104 tag,
105 ..
106 } => {
107 h.update(b"n:AsyncStore\0");
108 h.update(source.as_bytes());
109 h.update(destination.as_bytes());
110 h.update(tag.as_bytes());
111 }
112 Node::AsyncWait { tag } => {
113 h.update(b"n:AsyncWait\0");
114 h.update(tag.as_bytes());
115 }
116 Node::Trap { tag, .. } => {
117 h.update(b"n:Trap\0");
118 h.update(tag.as_bytes());
119 }
120 Node::Resume { tag } => {
121 h.update(b"n:Resume\0");
122 h.update(tag.as_bytes());
123 }
124 Node::AllReduce { buffer, op, group } => {
125 h.update(b"n:AllReduce\0");
126 h.update(buffer.as_bytes());
127 h.update(&op.builtin_wire_tag().to_le_bytes());
128 h.update(&group.as_u32().to_le_bytes());
129 }
130 Node::AllGather {
131 input,
132 output,
133 group,
134 } => {
135 h.update(b"n:AllGather\0");
136 h.update(input.as_bytes());
137 h.update(output.as_bytes());
138 h.update(&group.as_u32().to_le_bytes());
139 }
140 Node::ReduceScatter {
141 input,
142 output,
143 op,
144 group,
145 } => {
146 h.update(b"n:ReduceScatter\0");
147 h.update(input.as_bytes());
148 h.update(output.as_bytes());
149 h.update(&op.builtin_wire_tag().to_le_bytes());
150 h.update(&group.as_u32().to_le_bytes());
151 }
152 Node::Broadcast {
153 buffer,
154 root,
155 group,
156 } => {
157 h.update(b"n:Broadcast\0");
158 h.update(buffer.as_bytes());
159 h.update(&root.to_le_bytes());
160 h.update(&group.as_u32().to_le_bytes());
161 }
162 Node::Return => {
163 h.update(b"n:Return\0");
164 }
165 Node::Barrier { ordering } => {
166 h.update(b"n:Barrier\0");
167 mix_wire_fallback_hashable(h, ordering);
168 }
169 Node::Block(_) => {
170 h.update(b"n:Block\0");
171 }
172 Node::Region {
173 generator,
174 source_region,
175 ..
176 } => {
177 h.update(b"n:Region\0");
178 h.update(generator.as_bytes());
179 if let Some(source_gen) = source_region {
180 h.update(source_gen.name.as_bytes());
181 }
182 }
183 Node::Opaque(ext) => {
184 h.update(b"n:Opaque\0");
185 h.update(ext.extension_kind().as_bytes());
186 }
187 }
188 }
189}
190
191impl ExprVisitor for FallbackWireHasher<'_> {
192 fn visit_expr(&mut self, expr: &Expr) {
193 let h = &mut *self.0;
194 match expr {
195 Expr::LitU32(v) => {
196 h.update(b"e:LitU32\0");
197 h.update(&v.to_le_bytes());
198 }
199 Expr::LitI32(v) => {
200 h.update(b"e:LitI32\0");
201 h.update(&v.to_le_bytes());
202 }
203 Expr::LitF32(v) => {
204 h.update(b"e:LitF32\0");
205 h.update(&v.to_le_bytes());
206 }
207 Expr::LitBool(v) => {
208 h.update(b"e:LitBool\0");
209 h.update(&[u8::from(*v)]);
210 }
211 Expr::Var(name) => {
212 h.update(b"e:Var\0");
213 h.update(name.as_bytes());
214 }
215 Expr::Load { buffer, .. } => {
216 h.update(b"e:Load\0");
217 h.update(buffer.as_bytes());
218 }
219 Expr::BufLen { buffer } => {
220 h.update(b"e:BufLen\0");
221 h.update(buffer.as_bytes());
222 }
223 Expr::BufferRef { buffer } => {
224 h.update(b"e:BufferRef\0");
225 h.update(buffer.as_bytes());
226 }
227 Expr::InvocationId { axis } => {
228 h.update(b"e:InvocationId\0");
229 h.update(&[*axis]);
230 }
231 Expr::WorkgroupId { axis } => {
232 h.update(b"e:WorkgroupId\0");
233 h.update(&[*axis]);
234 }
235 Expr::LocalId { axis } => {
236 h.update(b"e:LocalId\0");
237 h.update(&[*axis]);
238 }
239 Expr::BinOp { op, .. } => {
240 h.update(b"e:BinOp\0");
241 mix_wire_fallback_hashable(h, op);
242 }
243 Expr::UnOp { op, .. } => {
244 h.update(b"e:UnOp\0");
245 mix_wire_fallback_hashable(h, op);
246 }
247 Expr::Call { op_id, .. } => {
248 h.update(b"e:Call\0");
249 h.update(op_id.as_bytes());
250 }
251 Expr::Select { .. } => {
252 h.update(b"e:Select\0");
253 }
254 Expr::Cast { target, .. } => {
255 h.update(b"e:Cast\0");
256 mix_wire_fallback_hashable(h, target);
257 }
258 Expr::Fma { .. } => {
259 h.update(b"e:Fma\0");
260 }
261 Expr::Atomic {
262 op,
263 buffer,
264 ordering,
265 ..
266 } => {
267 h.update(b"e:Atomic\0");
268 mix_wire_fallback_hashable(h, op);
269 h.update(buffer.as_bytes());
270 mix_wire_fallback_hashable(h, ordering);
271 }
272 Expr::SubgroupBallot { .. } => {
273 h.update(b"e:SubgroupBallot\0");
274 }
275 Expr::SubgroupShuffle { .. } => {
276 h.update(b"e:SubgroupShuffle\0");
277 }
278 Expr::SubgroupReduce { op, .. } => {
279 h.update(b"e:SubgroupReduce\0");
280 h.update(&[op.builtin_wire_tag()]);
281 }
282 Expr::SubgroupLocalId => {
283 h.update(b"e:SubgroupLocalId\0");
284 }
285 Expr::SubgroupSize => {
286 h.update(b"e:SubgroupSize\0");
287 }
288 Expr::Opaque(ext) => {
289 h.update(b"e:Opaque\0");
290 h.update(ext.extension_kind().as_bytes());
291 }
292 }
293 }
294}
295
296impl Program {
297 #[must_use]
307 pub fn reconcile_runnable_top_level(self) -> Self {
308 if self.is_top_level_region_wrapped() {
309 return self;
310 }
311 self.map_entry(Self::wrap_entry)
314 }
315
316 #[must_use]
318 #[inline]
319 pub fn buffer(&self, name: &str) -> Option<&super::BufferDecl> {
320 self.buffer_index
321 .get(name)
322 .and_then(|&index| self.buffers.get(index))
323 }
324
325 #[must_use]
327 #[inline]
328 pub fn buffers(&self) -> &[super::BufferDecl] {
329 self.buffers.as_ref()
330 }
331
332 #[must_use]
334 #[inline]
335 #[cfg(test)]
336 pub(crate) fn buffers_arc(&self) -> &Arc<[super::BufferDecl]> {
337 &self.buffers
338 }
339
340 #[must_use]
347 #[inline]
348 pub fn structural_eq(&self, other: &Self) -> bool {
349 if std::ptr::eq(self, other)
354 || (Arc::ptr_eq(&self.buffers, &other.buffers)
355 && Arc::ptr_eq(&self.entry, &other.entry)
356 && self.entry_op_id == other.entry_op_id
357 && self.non_composable_with_self == other.non_composable_with_self
358 && self.workgroup_size == other.workgroup_size)
359 {
360 return true;
361 }
362 self.entry_op_id == other.entry_op_id
363 && self.non_composable_with_self == other.non_composable_with_self
364 && buffers_equal_ignoring_declaration_order(&self.buffers, &other.buffers)
365 && self.workgroup_size == other.workgroup_size
366 && self.entry == other.entry
367 }
368
369 #[must_use]
371 #[inline]
372 pub fn workgroup_size(&self) -> [u32; 3] {
373 self.workgroup_size
374 }
375
376 #[must_use]
381 #[inline]
382 pub fn parallel_region_size(&self) -> [u32; 3] {
383 self.workgroup_size
384 }
385
386 #[must_use]
389 #[inline]
390 pub fn is_non_composable_with_self(&self) -> bool {
391 self.non_composable_with_self
392 }
393
394 #[must_use]
396 #[inline]
397 pub fn with_non_composable_with_self(mut self, flag: bool) -> Self {
398 self.non_composable_with_self = flag;
399 self.invalidate_caches_for(ProgramMutationProvenance::NonComposableFlag);
400 self
401 }
402
403 #[inline]
408 pub fn set_workgroup_size(&mut self, workgroup_size: [u32; 3]) {
409 self.workgroup_size = workgroup_size;
410 self.invalidate_caches_for(ProgramMutationProvenance::WorkgroupSize);
411 }
412
413 #[inline]
415 pub fn set_parallel_region_size(&mut self, parallel_region_size: [u32; 3]) {
416 self.workgroup_size = parallel_region_size;
417 self.invalidate_caches_for(ProgramMutationProvenance::ParallelRegionSize);
418 }
419
420 #[must_use]
422 #[inline]
423 pub fn entry(&self) -> &[Node] {
424 self.entry.as_ref().as_slice()
425 }
426
427 #[must_use]
429 #[inline]
430 pub fn entry_arc(&self) -> &Arc<Vec<Node>> {
431 &self.entry
432 }
433
434 #[must_use]
437 #[inline]
438 pub fn is_explicit_noop(&self) -> bool {
439 self.buffers().is_empty()
440 && matches!(self.entry(), [Node::Region { body, .. }] if body.is_empty())
441 }
442
443 #[must_use]
447 #[inline]
448 pub fn is_top_level_region_wrapped(&self) -> bool {
449 !self.entry.is_empty()
450 && self
451 .entry()
452 .iter()
453 .all(|node| matches!(node, Node::Region { .. }))
454 }
455
456 #[must_use]
458 pub fn top_level_region_violation_cause(&self) -> Option<String> {
459 if self.entry().is_empty() {
460 return Some("program entry has no top-level Region".to_string());
461 }
462
463 self.entry()
464 .iter()
465 .enumerate()
466 .find(|(_, node)| !matches!(node, Node::Region { .. }))
467 .map(|(index, node)| {
468 format!(
469 "program entry node {index} is `{}` instead of `Node::Region`",
470 crate::ir_inner::model::node::node_op_id(node)
471 )
472 })
473 }
474
475 #[must_use]
478 pub fn top_level_region_violation(&self) -> Option<String> {
479 self.top_level_region_violation_cause().map(|cause| {
480 format!(
481 "{cause}. Fix: construct runnable programs with Program::wrapped(...) or wrap the body in Node::Region before validation, interpretation, or dispatch."
482 )
483 })
484 }
485
486 #[must_use]
488 #[inline]
489 pub fn entry_mut(&mut self) -> &mut Vec<Node> {
490 self.invalidate_caches_for(ProgramMutationProvenance::EntryMutation);
491 Arc::make_mut(&mut self.entry)
492 }
493
494 #[must_use]
496 #[inline]
497 pub fn fingerprint(&self) -> [u8; 32] {
498 *self.fingerprint.get_or_init(|| {
499 let hash = self.compute_wire_hash();
500 let _ = self.hash.set(hash);
501 *hash.as_bytes()
502 })
503 }
504
505 #[must_use]
518 pub fn vsa_fingerprint(&self) -> Vec<u32> {
519 self.fingerprint()
520 .chunks_exact(core::mem::size_of::<u32>())
521 .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
522 .collect()
523 }
524
525 #[must_use]
527 #[inline]
528 pub fn output_buffer_indices(&self) -> &[u32] {
529 self.output_buffer_index
530 .get_or_init(|| {
531 Arc::new(
532 self.buffers()
533 .iter()
534 .enumerate()
535 .filter_map(|(index, buffer)| {
536 matches!(
537 buffer.access(),
538 BufferAccess::ReadWrite | BufferAccess::WriteOnly
539 )
540 .then(|| u32::try_from(index).ok())
541 .flatten()
542 })
543 .collect(),
544 )
545 })
546 .as_slice()
547 }
548
549 #[must_use]
551 #[inline]
552 pub fn has_indirect_dispatch(&self) -> bool {
553 *self.has_indirect_dispatch.get_or_init(|| {
554 if !self
560 .stats()
561 .has_any_node_kind(super::stats::NODE_KIND_INDIRECT_DISPATCH)
562 {
563 return false;
564 }
565 let mut stack: smallvec::SmallVec<[&Node; 32]> = self.entry().iter().rev().collect();
566 while let Some(node) = stack.pop() {
567 match node {
568 Node::IndirectDispatch { .. } => return true,
569 Node::If {
570 then, otherwise, ..
571 } => {
572 stack.extend(otherwise.iter().rev());
573 stack.extend(then.iter().rev());
574 }
575 Node::Loop { body, .. } | Node::Block(body) => {
576 stack.extend(body.iter().rev());
577 }
578 Node::Region { body, .. } => {
579 stack.extend(body.iter().rev());
580 }
581 Node::Let { .. }
582 | Node::Assign { .. }
583 | Node::Store { .. }
584 | Node::AllReduce { .. }
585 | Node::AllGather { .. }
586 | Node::ReduceScatter { .. }
587 | Node::Broadcast { .. }
588 | Node::Return
589 | Node::Barrier { .. }
590 | Node::AsyncLoad { .. }
591 | Node::AsyncStore { .. }
592 | Node::AsyncWait { .. }
593 | Node::Trap { .. }
594 | Node::Resume { .. }
595 | Node::Opaque(_) => {}
596 }
597 }
598 false
599 })
600 }
601
602 #[must_use]
604 #[inline]
605 pub fn has_buffer(&self, name: &str) -> bool {
606 self.buffer_index.contains_key(name)
607 }
608
609 #[must_use]
611 #[inline]
612 pub fn buffer_count(&self) -> usize {
613 self.buffers.len()
614 }
615
616 #[inline]
617 pub(super) fn build_buffer_index(
618 buffers: &[super::BufferDecl],
619 ) -> rustc_hash::FxHashMap<Arc<str>, usize> {
620 let mut index = rustc_hash::FxHashMap::default();
621 index.reserve(buffers.len());
622 for (buffer_index, buffer) in buffers.iter().enumerate() {
623 index
624 .entry(Arc::clone(&buffer.name))
625 .or_insert(buffer_index);
626 }
627 index
628 }
629
630 #[inline]
632 pub fn mark_structurally_validated(&self) {
633 self.structural_validation_fingerprint.store(
634 self.current_validation_fingerprint_token(),
635 Ordering::Release,
636 );
637 self.mutation_provenance
638 .store(ProgramMutationProvenance::Clean as u8, Ordering::Release);
639 self.structural_validated.store(true, Ordering::Release);
640 }
641
642 #[must_use]
644 #[inline]
645 pub fn is_structurally_validated(&self) -> bool {
646 if !self.structural_validated.load(Ordering::Acquire) {
647 return false;
648 }
649 if self.validation_mutation_provenance() == ProgramMutationProvenance::Unknown {
650 self.structural_validated.store(false, Ordering::Release);
651 return false;
652 }
653 let recorded = self
654 .structural_validation_fingerprint
655 .load(Ordering::Acquire);
656 if recorded == 0 || recorded != self.current_validation_fingerprint_token() {
657 self.structural_validated.store(false, Ordering::Release);
658 return false;
659 }
660 true
661 }
662
663 #[must_use]
665 #[inline]
666 pub fn validation_mutation_provenance(&self) -> ProgramMutationProvenance {
667 ProgramMutationProvenance::from_code(self.mutation_provenance.load(Ordering::Acquire))
668 }
669
670 #[inline]
674 pub fn mark_unknown_mutation_provenance(&mut self) {
675 self.invalidate_caches_for(ProgramMutationProvenance::Unknown);
676 }
677
678 #[inline]
680 pub fn mark_validated_on(&self, backend_id: &str) {
681 if self.validation_mutation_provenance() == ProgramMutationProvenance::Unknown {
682 return;
683 }
684 self.validation_set
685 .get_or_init(|| Arc::new(dashmap::DashSet::new()))
686 .insert(Arc::from(self.validation_cache_key(backend_id)));
687 }
688
689 #[must_use]
691 #[inline]
692 pub fn is_validated_on(&self, backend_id: &str) -> bool {
693 self.validation_set
694 .get()
695 .is_some_and(|set| set.contains(self.validation_cache_key(backend_id).as_str()))
696 }
697
698 pub fn validate(&self) -> crate::error::IrResult<()> {
705 if self.validation_mutation_provenance() == ProgramMutationProvenance::Unknown {
706 return Err(crate::error::IrError::WireFormatValidation {
707 message: "program validation cache was invalidated by unknown mutation provenance. Fix: rebuild the Program through Program::wrapped/from_wire or use a named Program mutation API before validating.".into(),
708 });
709 }
710 if self.is_structurally_validated() {
711 return Ok(());
712 }
713 let errors = crate::validate::validate(self);
714 if errors.is_empty() {
715 self.mark_structurally_validated();
716 return Ok(());
717 }
718 Err(crate::error::IrError::Validation { issues: errors })
719 }
720
721 #[inline]
722 #[must_use]
730 pub fn estimate_peak_vram_bytes(&self) -> u64 {
731 self.buffers
732 .iter()
733 .map(|buffer| {
734 let Some(element_size) = buffer.element.size_bytes() else {
735 return u64::MAX;
736 };
737 u64::from(buffer.count)
738 .saturating_mul(u64::try_from(element_size).unwrap_or(u64::MAX))
739 })
740 .fold(0u64, u64::saturating_add)
741 }
742
743 #[must_use]
745 pub fn peak_intensity(&self) -> OpIntensity {
746 let mut peak = OpIntensity::Free;
747 for node in self.entry() {
748 peak = peak.max(Self::node_intensity(node));
749 }
750 peak
751 }
752
753 fn node_intensity(node: &crate::ir::Node) -> OpIntensity {
754 use crate::ir::Node;
755 match node {
756 Node::Let { value, .. } | Node::Assign { value, .. } => Self::expr_intensity(value),
757 Node::Store { index, value, .. } => {
758 Self::expr_intensity(index).max(Self::expr_intensity(value))
759 }
760 Node::If {
761 cond,
762 then,
763 otherwise,
764 } => {
765 let mut p = Self::expr_intensity(cond);
766 for n in then {
767 p = p.max(Self::node_intensity(n));
768 }
769 for n in otherwise {
770 p = p.max(Self::node_intensity(n));
771 }
772 p
773 }
774 Node::Loop { from, to, body, .. } => {
775 let mut p = Self::expr_intensity(from).max(Self::expr_intensity(to));
776 for n in body {
777 p = p.max(Self::node_intensity(n));
778 }
779 p
780 }
781 Node::Block(nodes) => {
782 let mut p = OpIntensity::Free;
783 for n in nodes {
784 p = p.max(Self::node_intensity(n));
785 }
786 p
787 }
788 Node::Region { body, .. } => {
789 let mut p = OpIntensity::Free;
790 for n in body.iter() {
791 p = p.max(Self::node_intensity(n));
792 }
793 p
794 }
795 _ => OpIntensity::Free,
796 }
797 }
798
799 fn expr_intensity(expr: &crate::ir::Expr) -> OpIntensity {
800 use crate::ir::Expr;
801 match expr {
802 Expr::BinOp { op, left, right } => op
803 .intensity()
804 .max(Self::expr_intensity(left))
805 .max(Self::expr_intensity(right)),
806 Expr::UnOp { operand, .. } => Self::expr_intensity(operand),
807 Expr::Load { index, .. } => Self::expr_intensity(index),
808 Expr::Select {
809 cond,
810 true_val,
811 false_val,
812 } => Self::expr_intensity(cond)
813 .max(Self::expr_intensity(true_val))
814 .max(Self::expr_intensity(false_val)),
815 Expr::Cast { value, .. } => Self::expr_intensity(value),
816 Expr::Fma { a, b, c } => Self::expr_intensity(a)
817 .max(Self::expr_intensity(b))
818 .max(Self::expr_intensity(c)),
819 Expr::Atomic {
820 index,
821 value,
822 expected,
823 ..
824 } => {
825 let mut p = Self::expr_intensity(index).max(Self::expr_intensity(value));
826 if let Some(e) = expected {
827 p = p.max(Self::expr_intensity(e));
828 }
829 p.max(OpIntensity::Heavy)
830 }
831 Expr::SubgroupBallot { cond } => Self::expr_intensity(cond).max(OpIntensity::Heavy),
832 Expr::SubgroupShuffle { value, lane } => Self::expr_intensity(value)
833 .max(Self::expr_intensity(lane))
834 .max(OpIntensity::Heavy),
835 Expr::SubgroupReduce { value, .. } => {
836 Self::expr_intensity(value).max(OpIntensity::Heavy)
837 }
838 _ => OpIntensity::Free,
839 }
840 }
841
842 fn compute_wire_hash(&self) -> blake3::Hash {
843 match self.canonical_wire_hash() {
844 Ok(hash) => hash,
845 Err(error) => {
846 let structural = self.structural_fingerprint_fallback();
847 let err_msg = error.to_string();
848 let mut fallback = Vec::with_capacity(96 + err_msg.len() + structural.len());
849 fallback.extend_from_slice(b"VYRE-PROGRAM-CANONICAL-WIRE-HASH-ERROR\0");
850 fallback.extend_from_slice(err_msg.as_bytes());
851 fallback.push(0);
852 fallback.extend_from_slice(structural.as_bytes());
853 blake3::hash(&fallback)
854 }
855 }
856 }
857
858 fn structural_fingerprint_fallback(&self) -> String {
859 let mut hasher = blake3::Hasher::new();
860 hasher.update(b"VYRE-WIRE-FALLBACK-V4\0");
861 if let Some(id) = self.entry_op_id.as_deref() {
862 hasher.update(id.as_bytes());
863 }
864 hasher.update(b"\0");
865 for axis in &self.workgroup_size {
866 hasher.update(&axis.to_le_bytes());
867 }
868 hasher.update(&[u8::from(self.non_composable_with_self)]);
869 let mut keys: Vec<Vec<u8>> = self
870 .buffers()
871 .iter()
872 .map(buffer_decl_canonical_key)
873 .collect();
874 keys.sort_unstable();
875 for key in keys {
876 hasher.update(&key);
877 }
878 let mut visitor = FallbackWireHasher(&mut hasher);
879 walk_nodes_and_exprs(self, &mut visitor);
880 hasher.finalize().to_hex().to_string()
881 }
882
883 fn validation_cache_key(&self, backend_id: &str) -> String {
884 const HEX: &[u8; 16] = b"0123456789abcdef";
885 let fingerprint = self.current_validation_fingerprint();
886 let mut key = String::with_capacity(backend_id.len() + 1 + 64);
887 key.push_str(backend_id);
888 key.push(':');
889 for &byte in &fingerprint {
890 key.push(HEX[(byte >> 4) as usize] as char);
891 key.push(HEX[(byte & 0x0f) as usize] as char);
892 }
893 key
894 }
895
896 #[inline]
897 pub(super) fn invalidate_caches(&mut self) {
898 self.invalidate_caches_for(ProgramMutationProvenance::InternalShapeMutation);
899 }
900
901 #[inline]
902 pub(super) fn invalidate_caches_for(&mut self, provenance: ProgramMutationProvenance) {
903 self.structural_validated.store(false, Ordering::Release);
904 self.structural_validation_fingerprint
905 .store(0, Ordering::Release);
906 self.mutation_provenance
907 .store(provenance as u8, Ordering::Release);
908 if let Some(set) = self.validation_set.get() {
909 set.clear();
910 }
911 let _ = self.hash.take();
912 let _ = self.fingerprint.take();
913 let _ = self.normalized_cache_digest.take();
914 drop(self.output_buffer_index.take());
915 let _ = self.has_indirect_dispatch.take();
916 drop(self.stats.take());
917 }
918
919 fn current_validation_fingerprint(&self) -> [u8; 32] {
920 *self.compute_wire_hash().as_bytes()
921 }
922
923 fn current_validation_fingerprint_token(&self) -> u64 {
924 let bytes = self.current_validation_fingerprint();
925 let token = u64::from_le_bytes([
926 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
927 ]);
928 token.max(1)
929 }
930
931 #[inline]
932 pub(super) fn wrap_entry(entry: Vec<Node>) -> Vec<Node> {
933 if !Self::entry_needs_root_region(&entry) {
934 return entry;
935 }
936 vec![Node::Region {
937 generator: Ident::from(Self::ROOT_REGION_GENERATOR),
938 source_region: None,
939 body: Arc::new(entry),
940 }]
941 }
942
943 #[inline]
944 fn entry_needs_root_region(entry: &[Node]) -> bool {
945 entry.is_empty()
946 || entry
947 .iter()
948 .any(|node| !matches!(node, Node::Region { .. }))
949 }
950}
951
952pub(crate) fn buffers_equal_ignoring_declaration_order(
953 left: &[super::BufferDecl],
954 right: &[super::BufferDecl],
955) -> bool {
956 if left.len() != right.len() {
957 return false;
958 }
959
960 if left == right {
967 return true;
968 }
969
970 let mut left_keys = Vec::with_capacity(left.len());
971 left_keys.extend(left.iter().map(buffer_decl_canonical_key));
972 let mut right_keys = Vec::with_capacity(right.len());
973 right_keys.extend(right.iter().map(buffer_decl_canonical_key));
974 left_keys.sort_unstable();
975 right_keys.sort_unstable();
976 left_keys == right_keys
977}
978
979pub(super) fn buffer_decl_canonical_key(buffer: &super::BufferDecl) -> Vec<u8> {
1006 use crate::serial::wire::encode::to_wire::{linear_type_tag, put_shape_predicate};
1007 use crate::serial::wire::framing::{put_len_u32, put_u32, put_u8};
1008 use crate::serial::wire::tags::put_data_type;
1009
1010 let super::BufferDecl {
1013 name,
1014 binding,
1015 access,
1016 kind,
1017 element,
1018 count,
1019 is_output,
1020 pipeline_live_out,
1021 output_byte_range,
1022 hints,
1023 bytes_extraction,
1024 linear_type,
1025 shape_predicate,
1026 } = buffer;
1027
1028 let mut key = Vec::with_capacity(96);
1029 if let Err(error) = put_len_u32(&mut key, name.len(), "buffer name length") {
1030 key.extend_from_slice(b"\0name-length-error\0");
1031 key.extend_from_slice(error.as_bytes());
1032 }
1033 key.extend_from_slice(name.as_bytes());
1034 put_u32(&mut key, *binding);
1035 match crate::serial::wire::tags::access_tag::access_tag(access) {
1036 Ok(tag) => put_u8(&mut key, tag),
1037 Err(error) => {
1038 put_u8(&mut key, u8::MAX);
1039 key.extend_from_slice(error.as_bytes());
1040 }
1041 }
1042 put_u8(&mut key, super::cache_digest::memory_kind_cache_tag(*kind));
1043 if let Err(error) = put_data_type(&mut key, element) {
1044 key.extend_from_slice(b"\0dtype-error\0");
1045 key.extend_from_slice(error.as_bytes());
1046 }
1047 put_u32(&mut key, *count);
1048 put_u8(&mut key, u8::from(*is_output));
1049 put_u8(&mut key, u8::from(*pipeline_live_out));
1050 match output_byte_range {
1051 Some(range) => {
1052 put_u8(&mut key, 1);
1053 match u32::try_from(range.start) {
1054 Ok(start) => put_u32(&mut key, start),
1055 Err(error) => {
1056 put_u32(&mut key, u32::MAX);
1057 key.extend_from_slice(error.to_string().as_bytes());
1058 }
1059 }
1060 match u32::try_from(range.end) {
1061 Ok(end) => put_u32(&mut key, end),
1062 Err(error) => {
1063 put_u32(&mut key, u32::MAX);
1064 key.extend_from_slice(error.to_string().as_bytes());
1065 }
1066 }
1067 }
1068 None => put_u8(&mut key, 0),
1069 }
1070 match hints.coalesce_axis {
1071 Some(axis) => {
1072 put_u8(&mut key, 1);
1073 put_u8(&mut key, axis);
1074 }
1075 None => put_u8(&mut key, 0),
1076 }
1077 put_u32(&mut key, hints.preferred_alignment);
1078 put_u8(
1079 &mut key,
1080 match hints.cache_locality {
1081 super::CacheLocality::Streaming => 0,
1082 super::CacheLocality::Temporal => 1,
1083 super::CacheLocality::Random => 2,
1084 },
1085 );
1086 put_u8(&mut key, u8::from(*bytes_extraction));
1087 put_u8(&mut key, linear_type_tag(*linear_type));
1088 if let Err(error) = put_shape_predicate(&mut key, shape_predicate.as_ref(), 0) {
1089 key.extend_from_slice(b"\0shape-predicate-error\0");
1095 key.extend_from_slice(error.as_bytes());
1096 key.extend_from_slice(format!("{shape_predicate:?}").as_bytes());
1097 }
1098 key
1099}