1use std::collections::HashSet;
4use std::hash::BuildHasherDefault;
5
6use rustc_hash::FxHasher;
7use vyre_foundation::ir::{OpId, Program};
8use vyre_foundation::validate::{BackendValidationCapabilities, ValidationOptions};
9
10use crate::{BackendError, DispatchConfig, VyreBackend};
11
12pub const DEFAULT_VALIDATION_HASH_ENTRIES: usize = 8192;
14pub const DEFAULT_VALIDATION_VSA_ENTRIES: usize = 2048;
16pub const DEFAULT_VALIDATION_VSA_SHARDS: usize = 64;
18
19type ValidationSet = dashmap::DashSet<blake3::Hash, BuildHasherDefault<FxHasher>>;
20
21pub struct ValidationCache {
23 hashes: ValidationSet,
24 vsa_hashes: ValidationSet,
25 max_hash_entries: usize,
26 max_vsa_entries: usize,
27 vsa_shards: usize,
28}
29
30impl std::fmt::Debug for ValidationCache {
31 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 formatter
33 .debug_struct("ValidationCache")
34 .field("hashes", &self.hashes.len())
35 .field("vsa_hashes", &self.vsa_hashes.len())
36 .field("vsa_shards", &self.vsa_shards)
37 .field("max_hash_entries", &self.max_hash_entries)
38 .field("max_vsa_entries", &self.max_vsa_entries)
39 .finish()
40 }
41}
42
43impl Default for ValidationCache {
44 fn default() -> Self {
45 Self::new(
46 DEFAULT_VALIDATION_HASH_ENTRIES,
47 DEFAULT_VALIDATION_VSA_ENTRIES,
48 DEFAULT_VALIDATION_VSA_SHARDS,
49 )
50 }
51}
52
53impl ValidationCache {
54 #[must_use]
56 pub fn new(max_hash_entries: usize, max_vsa_entries: usize, vsa_shards: usize) -> Self {
57 let shard_count = vsa_shards.max(1);
58 Self {
59 hashes: dashmap::DashSet::with_hasher(BuildHasherDefault::<FxHasher>::default()),
60 vsa_hashes: dashmap::DashSet::with_capacity_and_hasher(
61 max_vsa_entries.max(1),
62 BuildHasherDefault::<FxHasher>::default(),
63 ),
64 max_hash_entries: max_hash_entries.max(1),
65 max_vsa_entries: max_vsa_entries.max(1),
66 vsa_shards: shard_count,
67 }
68 }
69
70 #[must_use]
72 pub fn program_hash(program: &Program) -> blake3::Hash {
73 blake3::Hash::from(program.fingerprint())
74 }
75
76 #[must_use]
78 pub fn contains_hash(&self, hash: &blake3::Hash) -> bool {
79 self.hashes.contains(hash)
80 }
81
82 pub fn remember_hash(&self, hash: blake3::Hash) {
84 if self.hashes.len() >= self.max_hash_entries {
85 self.hashes.clear();
86 }
87 self.hashes.insert(hash);
88 }
89
90 pub fn remember_success(&self, hash: blake3::Hash, vsa: &[u32]) -> Result<(), BackendError> {
96 self.remember_hash(hash);
97 if self.vsa_hashes.len() >= self.max_vsa_entries {
98 self.vsa_hashes.clear();
99 }
100 self.vsa_hashes.insert(vsa_words_hash(vsa));
101 Ok(())
102 }
103
104 pub fn clear(&self) -> Result<(), BackendError> {
110 self.hashes.clear();
111 self.vsa_hashes.clear();
112 Ok(())
113 }
114
115 pub fn get_or_validate(
126 &self,
127 program: &Program,
128 validation_options: ValidationOptions<'_>,
129 supported_ops: &HashSet<OpId>,
130 caps: ProgramValidationCaps,
131 ) -> Result<(), BackendError> {
132 let hash = Self::program_hash(program);
133 if self.contains_hash(&hash) || program.is_validated_on(caps.backend_id) {
134 self.remember_hash(hash);
135 return Ok(());
136 }
137
138 validate_program_contract(program, validation_options, supported_ops, caps)?;
139
140 let vsa = crate::launch::program_vsa_fingerprint_words(program);
141 self.remember_success(hash, &vsa)?;
142 program.mark_validated_on(caps.backend_id);
143 Ok(())
144 }
145
146 pub fn get_or_validate_backend<B>(
157 &self,
158 program: &Program,
159 backend: &B,
160 ) -> Result<(), BackendError>
161 where
162 B: VyreBackend + BackendValidationCapabilities,
163 {
164 let validation_options = ValidationOptions::default().with_backend(backend);
165 self.get_or_validate(
166 program,
167 validation_options,
168 backend.supported_ops(),
169 ProgramValidationCaps::from_backend(backend),
170 )
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub struct ProgramValidationCaps {
177 pub backend_id: &'static str,
179 pub supports_subgroup_ops: bool,
181 pub supports_f16: bool,
183 pub supports_bf16: bool,
185 pub supports_indirect_dispatch: bool,
187 pub supports_distributed_collectives: bool,
189 pub supports_trap_propagation: bool,
191 pub max_workgroup_size: [u32; 3],
193}
194
195impl ProgramValidationCaps {
196 #[must_use]
198 pub fn from_backend(backend: &dyn VyreBackend) -> Self {
199 Self {
200 backend_id: backend.id(),
201 supports_subgroup_ops: backend.supports_subgroup_ops(),
202 supports_f16: backend.supports_f16(),
203 supports_bf16: backend.supports_bf16(),
204 supports_indirect_dispatch: backend.supports_indirect_dispatch(),
205 supports_distributed_collectives: backend.supports_distributed_collectives(),
206 supports_trap_propagation: true,
207 max_workgroup_size: backend.max_workgroup_size(),
208 }
209 }
210}
211
212pub fn validate_program_contract(
219 program: &Program,
220 validation_options: ValidationOptions<'_>,
221 supported_ops: &HashSet<OpId>,
222 caps: ProgramValidationCaps,
223) -> Result<(), BackendError> {
224 let lowered_program = if caps.supports_distributed_collectives {
225 None
226 } else {
227 vyre_foundation::transform::collectives::lower_single_rank_collectives(program).map_err(
228 |error| BackendError::InvalidProgram {
229 fix: error.to_string(),
230 },
231 )?
232 };
233 let program = lowered_program.as_ref().unwrap_or(program);
234 let report = vyre_foundation::validate::validate_with_options(program, validation_options);
235 if let Some(source) = report.errors.into_iter().next() {
236 return Err(BackendError::Validation { source });
237 }
238
239 validate_supported_ops(program, caps.backend_id, supported_ops)
240 .map_err(|source| BackendError::Validation { source })?;
241
242 let required = vyre_foundation::program_caps::scan(program);
243 vyre_foundation::program_caps::check_backend_capabilities(
244 caps.backend_id,
245 caps.supports_subgroup_ops,
246 caps.supports_f16,
247 caps.supports_bf16,
248 caps.supports_indirect_dispatch,
249 caps.supports_trap_propagation,
250 caps.supports_distributed_collectives,
251 caps.max_workgroup_size,
252 &required,
253 )
254 .map_err(|error| BackendError::InvalidProgram {
255 fix: error.to_string(),
256 })
257}
258
259fn validate_supported_ops(
260 program: &Program,
261 backend_id: &'static str,
262 supported_ops: &HashSet<OpId>,
263) -> Result<(), vyre_foundation::ir::ValidationError> {
264 struct SupportedOpsBackend<'a> {
265 id: &'static str,
266 ops: &'a HashSet<OpId>,
267 }
268
269 impl crate::backend::Backend for SupportedOpsBackend<'_> {
270 fn id(&self) -> &'static str {
271 self.id
272 }
273
274 fn version(&self) -> &'static str {
275 env!("CARGO_PKG_VERSION")
276 }
277
278 fn supported_ops(&self) -> &HashSet<OpId> {
279 self.ops
280 }
281 }
282
283 crate::backend::validation::validate_program(
284 program,
285 &SupportedOpsBackend {
286 id: backend_id,
287 ops: supported_ops,
288 },
289 )
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub struct LaunchGeometryLimits {
295 pub backend: &'static str,
297 pub max_threads_per_block: u32,
299 pub max_block_dim: [u32; 3],
301 pub max_grid_dim: [u32; 3],
303 pub max_threads_per_sm: u32,
313}
314
315impl LaunchGeometryLimits {
316 #[must_use]
319 pub fn blocks_per_compute_unit(&self, workgroup_threads: u32) -> Option<u32> {
320 (self.max_threads_per_sm != 0 && workgroup_threads != 0)
321 .then(|| blocks_per_compute_unit(self.max_threads_per_sm, workgroup_threads))
322 }
323
324 #[must_use]
327 pub fn resident_threads_per_compute_unit(&self, workgroup_threads: u32) -> Option<u32> {
328 (self.max_threads_per_sm != 0 && workgroup_threads != 0)
329 .then(|| resident_threads_per_compute_unit(self.max_threads_per_sm, workgroup_threads))
330 }
331}
332
333#[must_use]
354pub fn blocks_per_compute_unit(max_threads_per_unit: u32, workgroup_threads: u32) -> u32 {
355 if workgroup_threads == 0 {
356 return 0;
357 }
358 max_threads_per_unit / workgroup_threads
359}
360
361#[must_use]
371pub fn resident_threads_per_compute_unit(max_threads_per_unit: u32, workgroup_threads: u32) -> u32 {
372 blocks_per_compute_unit(max_threads_per_unit, workgroup_threads)
373 .saturating_mul(workgroup_threads)
374}
375
376pub fn validate_launch_geometry(
383 workgroup: [u32; 3],
384 grid: [u32; 3],
385 limits: LaunchGeometryLimits,
386) -> Result<(), BackendError> {
387 if workgroup.contains(&0) || grid.contains(&0) {
388 return Err(BackendError::InvalidProgram {
389 fix: format!(
390 "Fix: {} workgroup and grid dimensions must all be non-zero.",
391 limits.backend
392 ),
393 });
394 }
395 let threads = workgroup[0]
396 .checked_mul(workgroup[1])
397 .and_then(|xy| xy.checked_mul(workgroup[2]))
398 .ok_or_else(|| BackendError::InvalidProgram {
399 fix: format!(
400 "Fix: {} workgroup dimensions overflowed u32; reduce workgroup_override.",
401 limits.backend
402 ),
403 })?;
404 if threads > limits.max_threads_per_block {
405 return Err(BackendError::InvalidProgram {
406 fix: format!(
407 "Fix: {} workgroup has {threads} threads but device max is {}.",
408 limits.backend, limits.max_threads_per_block
409 ),
410 });
411 }
412 for (axis, &dim) in workgroup.iter().enumerate() {
413 if dim > limits.max_block_dim[axis] {
414 return Err(BackendError::InvalidProgram {
415 fix: format!(
416 "Fix: {} workgroup axis {axis} requested {} threads but device max is {}.",
417 limits.backend, dim, limits.max_block_dim[axis]
418 ),
419 });
420 }
421 }
422 for (axis, &dim) in grid.iter().enumerate() {
423 if dim > limits.max_grid_dim[axis] {
424 return Err(BackendError::InvalidProgram {
425 fix: format!(
426 "Fix: {} grid axis {axis} requested {} workgroups but device max is {}.",
427 limits.backend, dim, limits.max_grid_dim[axis]
428 ),
429 });
430 }
431 }
432 Ok(())
433}
434
435pub fn validate_program_for_backend(
445 backend: &dyn VyreBackend,
446 program: &Program,
447 config: &DispatchConfig,
448) -> Result<(), BackendError> {
449 let workgroup = config
450 .workgroup_override
451 .unwrap_or(program.workgroup_size());
452 let max_axes = backend.max_workgroup_size();
453 if workgroup.contains(&0) {
454 return Err(BackendError::InvalidProgram {
455 fix: format!(
456 "Fix: backend `{}` cannot dispatch zero-sized workgroup dimensions; set positive workgroup sizes.",
457 backend.id()
458 ),
459 });
460 }
461 for (axis, &dim) in workgroup.iter().enumerate() {
462 if dim > max_axes[axis] {
463 return Err(BackendError::InvalidProgram {
464 fix: format!(
465 "Fix: backend `{}` workgroup axis {axis} requested {} but max is {}.",
466 backend.id(),
467 dim,
468 max_axes[axis]
469 ),
470 });
471 }
472 }
473 let invocations = workgroup[0]
474 .checked_mul(workgroup[1])
475 .and_then(|xy| xy.checked_mul(workgroup[2]))
476 .ok_or_else(|| BackendError::InvalidProgram {
477 fix: format!(
478 "Fix: backend `{}` workgroup dimensions overflowed u32; reduce workgroup size.",
479 backend.id()
480 ),
481 })?;
482 let max_invocations = backend.max_compute_invocations_per_workgroup();
483 if invocations > max_invocations {
484 return Err(BackendError::InvalidProgram {
485 fix: format!(
486 "Fix: backend `{}` workgroup has {invocations} invocations but max is {max_invocations}.",
487 backend.id()
488 ),
489 });
490 }
491 if let Some(grid) = config.grid_override {
492 let max_workgroups = backend.max_compute_workgroups_per_dimension();
493 if grid.contains(&0) {
494 return Err(BackendError::InvalidProgram {
495 fix: format!(
496 "Fix: backend `{}` cannot dispatch zero-sized grid dimensions; set positive grid_override values.",
497 backend.id()
498 ),
499 });
500 }
501 for (axis, &dim) in grid.iter().enumerate() {
502 if dim > max_workgroups {
503 return Err(BackendError::InvalidProgram {
504 fix: format!(
505 "Fix: backend `{}` grid_override axis {axis} requested {} workgroups but max is {}.",
506 backend.id(),
507 dim,
508 max_workgroups
509 ),
510 });
511 }
512 }
513 }
514 Ok(())
515}
516
517fn vsa_words_hash(words: &[u32]) -> blake3::Hash {
518 let mut hasher = blake3::Hasher::new();
519 hasher.update(&(words.len() as u64).to_le_bytes());
520 for word in words {
521 hasher.update(&word.to_le_bytes());
522 }
523 hasher.finalize()
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 #[test]
531 fn validation_cache_records_vsa_without_lock_shards() {
532 let cache = ValidationCache::new(8, 8, 4);
533 let hash = blake3::hash(b"program");
534 cache
535 .remember_success(hash, &[1, 2, 3, 4])
536 .expect("Fix: lock-free VSA cache insertion must not fail");
537
538 assert!(cache.contains_hash(&hash));
539 assert_eq!(cache.vsa_hashes.len(), 1);
540 assert!(format!("{cache:?}").contains("vsa_hashes"));
541 }
542
543 #[test]
544 fn validation_cache_bounds_vsa_hashes_by_clear() {
545 let cache = ValidationCache::new(8, 2, 4);
546 for i in 0..3u32 {
547 cache
548 .remember_success(blake3::hash(&i.to_le_bytes()), &[i])
549 .expect("Fix: VSA cache insertion must stay infallible");
550 }
551 assert!(
552 cache.vsa_hashes.len() <= 2,
553 "Fix: bounded VSA cache must not grow past max entries"
554 );
555 }
556
557 #[test]
569 fn residency_division_is_integral_at_both_edges() {
570 assert_eq!(blocks_per_compute_unit(1536, 0), 0);
571 assert_eq!(resident_threads_per_compute_unit(1536, 0), 0);
572 assert_eq!(blocks_per_compute_unit(1536, 2048), 0);
573 assert_eq!(resident_threads_per_compute_unit(1536, 2048), 0);
574 assert_eq!(blocks_per_compute_unit(0, 256), 0);
575 assert_eq!(resident_threads_per_compute_unit(0, 256), 0);
576
577 assert_eq!(blocks_per_compute_unit(1536, 1024), 1);
578 assert_eq!(
579 resident_threads_per_compute_unit(1536, 1024),
580 1024,
581 "Fix: 1024 wide against a 1536-thread unit strands 512 slots. The truncation is the whole point of pinning this."
582 );
583 assert_eq!(blocks_per_compute_unit(1536, 256), 6);
584 assert_eq!(resident_threads_per_compute_unit(1536, 256), 1536);
585 }
586
587 #[test]
590 fn unreported_per_unit_budget_answers_unknown_rather_than_zero() {
591 let reported = LaunchGeometryLimits {
592 backend: "reported",
593 max_threads_per_block: 1024,
594 max_block_dim: [1024, 1024, 64],
595 max_grid_dim: [u32::MAX, u32::MAX, u32::MAX],
596 max_threads_per_sm: 1536,
597 };
598 let unreported = LaunchGeometryLimits {
599 max_threads_per_sm: 0,
600 ..reported
601 };
602
603 assert_eq!(reported.blocks_per_compute_unit(256), Some(6));
604 assert_eq!(reported.resident_threads_per_compute_unit(256), Some(1536));
605 assert_eq!(reported.blocks_per_compute_unit(0), None);
606 assert_eq!(unreported.blocks_per_compute_unit(256), None);
607 assert_eq!(unreported.resident_threads_per_compute_unit(256), None);
608 }
609}