1use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5use std::sync::{Mutex, OnceLock};
6
7use vyre_foundation::ir::{MemoryKind, Node, Program};
8
9use crate::binding::Binding;
10use crate::program_walks::{
11 dispatch_element_count_for_program, infer_dispatch_grid_for_count,
12 program_uses_launch_geometry_ids, try_dispatch_param_words_into,
13};
14use crate::tuner::{
15 identity_fisher_q16, Mode, NaturalGradientPolicy, Tuner, TunerCache, TuningMeasurement,
16 WORKGROUP_CANDIDATES,
17};
18use crate::validation::{validate_launch_geometry, LaunchGeometryLimits};
19use crate::{BackendError, DispatchConfig};
20
21const COLD_START_GRID_STEP_NS: u64 = 1_024;
22const COLD_START_IDLE_LANE_NS: u64 = 8;
23const COLD_START_TEMPERATURE_NS: u64 = 4_096;
24const MAX_NATURAL_LAUNCH_CACHE_ENTRIES: usize = 4_096;
25
26static NATURAL_LAUNCH_CACHE: OnceLock<Mutex<BTreeMap<NaturalLaunchCacheKey, NaturalLaunchEntry>>> =
27 OnceLock::new();
28
29#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct LaunchPlan {
32 pub element_count: u32,
34 pub workgroup: [u32; 3],
36 pub grid: [u32; 3],
38 pub param_words: Vec<u32>,
40 pub max_binding_alignment: usize,
45}
46
47impl LaunchPlan {
48 #[must_use]
50 pub fn new() -> Self {
51 Self {
52 element_count: 1,
53 workgroup: [1, 1, 1],
54 grid: [1, 1, 1],
55 param_words: Vec::new(),
56 max_binding_alignment: 1,
57 }
58 }
59
60 pub fn from_bindings(
67 program: &Program,
68 bindings: &[Binding],
69 config: &DispatchConfig,
70 limits: LaunchGeometryLimits,
71 ) -> Result<Self, BackendError> {
72 let mut plan = Self::new();
73 plan.prepare_into(program, bindings, config, limits)?;
74 Ok(plan)
75 }
76
77 pub fn prepare_into(
84 &mut self,
85 program: &Program,
86 bindings: &[Binding],
87 config: &DispatchConfig,
88 limits: LaunchGeometryLimits,
89 ) -> Result<(), BackendError> {
90 self.prepare_into_for_mode(program, bindings, config, limits, Mode::from_env())
91 }
92
93 fn prepare_into_for_mode(
94 &mut self,
95 program: &Program,
96 bindings: &[Binding],
97 config: &DispatchConfig,
98 limits: LaunchGeometryLimits,
99 mode: Mode,
100 ) -> Result<(), BackendError> {
101 let workgroup =
102 effective_launch_workgroup_for_mode(program, bindings, config, limits, mode);
103 validate_launch_geometry(workgroup, [1, 1, 1], limits)?;
104 let element_count = launch_element_count(program, bindings, workgroup, config, limits)?;
105 let grid = match config.grid_override {
106 Some(grid) => grid,
107 None => {
108 if workgroup[1] != 1 || workgroup[2] != 1 {
114 return Err(BackendError::InvalidProgram {
115 fix: format!(
116 "Fix: backend `{}` requires DispatchConfig::grid_override for non-1D workgroups. \
117 workgroup={:?} has no unambiguous default grid; set grid_override to the logical [x, y, z] you want.",
118 limits.backend, workgroup,
119 ),
120 });
121 }
122 infer_dispatch_grid_for_count(element_count, workgroup)?
123 }
124 };
125 validate_launch_geometry(workgroup, grid, limits)?;
126 self.element_count = element_count;
127 self.workgroup = workgroup;
128 self.grid = grid;
129 self.max_binding_alignment = bindings
130 .iter()
131 .map(|binding| binding.preferred_alignment)
132 .max()
133 .unwrap_or(1);
134 try_dispatch_param_words_into(bindings, element_count, &mut self.param_words).map_err(
135 |error| BackendError::InvalidProgram {
136 fix: format!(
137 "Fix: {}: dispatch ABI parameter staging failed: {error}",
138 limits.backend
139 ),
140 },
141 )?;
142 Ok(())
143 }
144}
145
146impl Default for LaunchPlan {
147 fn default() -> Self {
148 Self::new()
149 }
150}
151
152fn launch_element_count(
153 program: &Program,
154 bindings: &[Binding],
155 workgroup: [u32; 3],
156 config: &DispatchConfig,
157 limits: LaunchGeometryLimits,
158) -> Result<u32, BackendError> {
159 let inferred = dispatch_element_count_for_program(program, bindings);
160 let Some(grid) = config.grid_override else {
161 return Ok(inferred);
162 };
163 if workgroup.contains(&0) || grid.contains(&0) {
164 return Err(BackendError::InvalidProgram {
165 fix: format!(
166 "Fix: {} grid_override and workgroup dimensions must all be non-zero.",
167 limits.backend
168 ),
169 });
170 }
171 grid[0]
172 .checked_mul(workgroup[0])
173 .filter(|count| *count != 0)
174 .ok_or_else(|| BackendError::InvalidProgram {
175 fix: format!(
176 "Fix: {} grid_override.x * workgroup_size.x must fit in u32.",
177 limits.backend
178 ),
179 })
180}
181
182fn effective_launch_workgroup_for_mode(
183 program: &Program,
184 bindings: &[Binding],
185 config: &DispatchConfig,
186 limits: LaunchGeometryLimits,
187 mode: Mode,
188) -> [u32; 3] {
189 let element_count = dispatch_element_count_for_program(program, bindings);
190 resolve_launch_workgroup_for_mode(program, config, limits, element_count, mode)
191}
192
193#[must_use]
200pub fn resolve_launch_workgroup(
201 program: &Program,
202 config: &DispatchConfig,
203 limits: LaunchGeometryLimits,
204 element_count: u32,
205) -> [u32; 3] {
206 resolve_launch_workgroup_for_mode(program, config, limits, element_count, Mode::from_env())
207}
208
209#[must_use]
214pub fn resolve_launch_workgroup_for_mode(
215 program: &Program,
216 config: &DispatchConfig,
217 limits: LaunchGeometryLimits,
218 element_count: u32,
219 mode: Mode,
220) -> [u32; 3] {
221 if let Some(workgroup) = config.workgroup_override {
222 return workgroup;
223 }
224 let declared = program.workgroup_size();
225 if mode != Mode::NaturalGradient || config.grid_override.is_some() {
226 return declared;
227 }
228 natural_gradient_cold_start_workgroup(program, declared, element_count, limits)
229 .unwrap_or(declared)
230}
231
232#[must_use]
241pub fn record_launch_measurement(
242 program: &Program,
243 config: &DispatchConfig,
244 limits: LaunchGeometryLimits,
245 element_count: u32,
246 observed_workgroup: [u32; 3],
247 elapsed_ns: u64,
248) -> bool {
249 record_launch_measurement_for_mode(
250 program,
251 config,
252 limits,
253 element_count,
254 observed_workgroup,
255 elapsed_ns,
256 Mode::from_env(),
257 )
258}
259
260fn record_launch_measurement_for_mode(
261 program: &Program,
262 config: &DispatchConfig,
263 limits: LaunchGeometryLimits,
264 element_count: u32,
265 observed_workgroup: [u32; 3],
266 elapsed_ns: u64,
267 mode: Mode,
268) -> bool {
269 record_launch_measurement_for_mode_with_store(
270 program,
271 config,
272 limits,
273 element_count,
274 observed_workgroup,
275 elapsed_ns,
276 mode,
277 None,
278 )
279}
280
281fn record_launch_measurement_for_mode_with_store(
282 program: &Program,
283 config: &DispatchConfig,
284 limits: LaunchGeometryLimits,
285 element_count: u32,
286 observed_workgroup: [u32; 3],
287 elapsed_ns: u64,
288 mode: Mode,
289 persistent_path: Option<&Path>,
290) -> bool {
291 if mode != Mode::NaturalGradient
292 || elapsed_ns == 0
293 || config.workgroup_override.is_some()
294 || config.grid_override.is_some()
295 || observed_workgroup[1] != 1
296 || observed_workgroup[2] != 1
297 || !candidate_x_fits_limits(observed_workgroup[0], limits)
298 {
299 return false;
300 }
301 let declared = program.workgroup_size();
302 if !is_natural_gradient_launch_tunable(program, declared, element_count) {
303 return false;
304 }
305 let cache_key = NaturalLaunchCacheKey::new(program, declared, element_count, limits);
306 let mut measurements = natural_launch_cache_measurements(cache_key).unwrap_or_default();
307 measurements
308 .entry(observed_workgroup)
309 .and_modify(|best_ns| *best_ns = (*best_ns).min(elapsed_ns))
310 .or_insert(elapsed_ns);
311 let Some(selected) =
312 select_natural_launch_workgroup(declared, element_count, limits, Some(&measurements))
313 else {
314 return false;
315 };
316 natural_launch_cache_set(
317 cache_key,
318 NaturalLaunchEntry {
319 selected,
320 measurements,
321 },
322 );
323 if let Err(error) =
324 persist_natural_launch_selection(cache_key, limits, selected, persistent_path)
325 {
326 tracing::debug!(
327 error,
328 "natural-gradient launch feedback accepted in memory but could not persist"
329 );
330 }
331 true
332}
333
334fn natural_gradient_cold_start_workgroup(
335 program: &Program,
336 declared: [u32; 3],
337 element_count: u32,
338 limits: LaunchGeometryLimits,
339) -> Option<[u32; 3]> {
340 natural_gradient_cold_start_workgroup_with_store(program, declared, element_count, limits, None)
341}
342
343fn natural_gradient_cold_start_workgroup_with_store(
344 program: &Program,
345 declared: [u32; 3],
346 element_count: u32,
347 limits: LaunchGeometryLimits,
348 persistent_path: Option<&Path>,
349) -> Option<[u32; 3]> {
350 if !is_natural_gradient_launch_tunable(program, declared, element_count) {
351 return None;
352 }
353 let cache_key = NaturalLaunchCacheKey::new(program, declared, element_count, limits);
354 if let Some(cached) = natural_launch_cache_get(cache_key) {
355 return Some(cached);
356 }
357 if let Some(persisted) = natural_launch_cache_get_persisted(cache_key, limits, persistent_path)
358 {
359 natural_launch_cache_set(
360 cache_key,
361 NaturalLaunchEntry {
362 selected: persisted,
363 measurements: BTreeMap::new(),
364 },
365 );
366 return Some(persisted);
367 }
368
369 let selected = select_natural_launch_workgroup(declared, element_count, limits, None)?;
370 natural_launch_cache_set(
371 cache_key,
372 NaturalLaunchEntry {
373 selected,
374 measurements: BTreeMap::new(),
375 },
376 );
377 Some(selected)
378}
379
380fn select_natural_launch_workgroup(
381 declared: [u32; 3],
382 element_count: u32,
383 limits: LaunchGeometryLimits,
384 measurements: Option<&BTreeMap<[u32; 3], u64>>,
385) -> Option<[u32; 3]> {
386 let peak_resident = peak_resident_threads_per_compute_unit(declared[0], limits);
387 let mut samples = Vec::with_capacity(WORKGROUP_CANDIDATES.len() + 1);
388 for candidate_x in WORKGROUP_CANDIDATES
389 .iter()
390 .copied()
391 .chain(std::iter::once(declared[0]))
392 {
393 if !candidate_x_fits_limits(candidate_x, limits)
394 || samples
395 .iter()
396 .any(|sample: &TuningMeasurement| sample.workgroup_size[0] == candidate_x)
397 {
398 continue;
399 }
400 let workgroup_size = [candidate_x, 1, 1];
401 let elapsed_ns = match measurements.and_then(|measured| measured.get(&workgroup_size)) {
402 Some(&measured_ns) => measured_ns,
403 None if cold_start_admits_width(candidate_x, limits, peak_resident) => {
404 estimate_cold_start_latency_ns(element_count, candidate_x)
405 }
406 None => continue,
407 };
408 samples.push(TuningMeasurement {
409 workgroup_size,
410 elapsed_ns,
411 });
412 }
413 if let Some(measured) = measurements {
414 for (&workgroup_size, &elapsed_ns) in measured {
415 if workgroup_size[1] != 1
416 || workgroup_size[2] != 1
417 || elapsed_ns == 0
418 || !candidate_x_fits_limits(workgroup_size[0], limits)
419 || samples
420 .iter()
421 .any(|sample| sample.workgroup_size == workgroup_size)
422 {
423 continue;
424 }
425 samples.push(TuningMeasurement {
426 workgroup_size,
427 elapsed_ns,
428 });
429 }
430 }
431
432 if samples.len() < 2 {
433 return None;
434 }
435 NaturalGradientPolicy {
436 temperature_ns: COLD_START_TEMPERATURE_NS,
437 }
438 .suggest(&samples, &identity_fisher_q16(samples.len()))
439 .ok()
440 .map(|step| step.selected_workgroup_size)
441}
442
443#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
444struct NaturalLaunchCacheKey {
445 fingerprint: [u8; 32],
446 declared: [u32; 3],
447 element_count: u32,
448 max_threads_per_block: u32,
449 max_block_dim: [u32; 3],
450 max_grid_dim: [u32; 3],
451 max_threads_per_sm: u32,
452}
453
454impl NaturalLaunchCacheKey {
455 fn new(
456 program: &Program,
457 declared: [u32; 3],
458 element_count: u32,
459 limits: LaunchGeometryLimits,
460 ) -> Self {
461 Self {
462 fingerprint: program.fingerprint(),
463 declared,
464 element_count,
465 max_threads_per_block: limits.max_threads_per_block,
466 max_block_dim: limits.max_block_dim,
467 max_grid_dim: limits.max_grid_dim,
468 max_threads_per_sm: limits.max_threads_per_sm,
469 }
470 }
471
472 fn persistent_key(self) -> String {
473 let mut hasher = blake3::Hasher::new();
474 hasher.update(b"vyre-natural-launch-feedback-v2\0");
478 hasher.update(&self.fingerprint);
479 for axis in self.declared {
480 hasher.update(&axis.to_le_bytes());
481 }
482 hasher.update(&self.element_count.to_le_bytes());
483 hasher.update(&self.max_threads_per_block.to_le_bytes());
484 for axis in self.max_block_dim {
485 hasher.update(&axis.to_le_bytes());
486 }
487 for axis in self.max_grid_dim {
488 hasher.update(&axis.to_le_bytes());
489 }
490 hasher.update(&self.max_threads_per_sm.to_le_bytes());
491 let digest = hasher.finalize();
492 let mut key = String::with_capacity(74);
493 key.push_str("launch-v2-");
494 crate::pipeline::hashing::push_lower_hex(digest.as_bytes(), &mut key);
495 key
496 }
497}
498
499#[derive(Clone, Debug, Eq, PartialEq)]
500
501struct NaturalLaunchEntry {
502 selected: [u32; 3],
503 measurements: BTreeMap<[u32; 3], u64>,
504}
505
506fn natural_launch_cache_get(key: NaturalLaunchCacheKey) -> Option<[u32; 3]> {
507 let cache = NATURAL_LAUNCH_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()));
508 let guard = cache.lock().unwrap_or_else(|poison| poison.into_inner());
509 guard.get(&key).map(|entry| entry.selected)
510}
511
512fn natural_launch_cache_measurements(
513 key: NaturalLaunchCacheKey,
514) -> Option<BTreeMap<[u32; 3], u64>> {
515 let cache = NATURAL_LAUNCH_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()));
516 let guard = cache.lock().unwrap_or_else(|poison| poison.into_inner());
517 guard.get(&key).map(|entry| entry.measurements.clone())
518}
519
520fn natural_launch_cache_set(key: NaturalLaunchCacheKey, value: NaturalLaunchEntry) {
521 let cache = NATURAL_LAUNCH_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()));
522 let mut guard = cache.lock().unwrap_or_else(|poison| poison.into_inner());
523 if guard.len() >= MAX_NATURAL_LAUNCH_CACHE_ENTRIES && !guard.contains_key(&key) {
524 if let Some(oldest) = guard.keys().next().copied() {
525 guard.remove(&oldest);
526 }
527 }
528 guard.insert(key, value);
529}
530
531#[cfg(test)]
532fn natural_launch_cache_remove(key: NaturalLaunchCacheKey) {
533 if let Some(cache) = NATURAL_LAUNCH_CACHE.get() {
534 if let Ok(mut guard) = cache.lock() {
535 guard.remove(&key);
536 }
537 }
538}
539
540fn natural_launch_cache_get_persisted(
541 key: NaturalLaunchCacheKey,
542 limits: LaunchGeometryLimits,
543 persistent_path: Option<&Path>,
544) -> Option<[u32; 3]> {
545 let path = persistent_path
546 .map(Path::to_path_buf)
547 .unwrap_or_else(|| natural_launch_persistent_cache_path(limits));
548 let selected = TunerCache::load(&path).ok()?.get(&key.persistent_key())?;
549 valid_persisted_launch_selection(selected, limits).then_some(selected)
550}
551
552fn persist_natural_launch_selection(
553 key: NaturalLaunchCacheKey,
554 limits: LaunchGeometryLimits,
555 selected: [u32; 3],
556 persistent_path: Option<&Path>,
557) -> Result<(), String> {
558 let path = persistent_path
559 .map(Path::to_path_buf)
560 .unwrap_or_else(|| natural_launch_persistent_cache_path(limits));
561 persist_natural_launch_selection_to_path(key, selected, &path)
562}
563
564fn persist_natural_launch_selection_to_path(
565 key: NaturalLaunchCacheKey,
566 selected: [u32; 3],
567 path: &Path,
568) -> Result<(), String> {
569 let mut cache = TunerCache::load(path)?;
570 while cache.entries.len() >= MAX_NATURAL_LAUNCH_CACHE_ENTRIES {
571 let Some(oldest) = cache.entries.keys().next().cloned() else {
572 break;
573 };
574 cache.entries.remove(&oldest);
575 }
576 cache.set(key.persistent_key(), selected);
577 cache.save(path)
578}
579
580fn natural_launch_persistent_cache_path(limits: LaunchGeometryLimits) -> PathBuf {
581 Tuner::cache_path_for_adapter(&natural_launch_persistent_adapter_key(limits))
582}
583
584fn natural_launch_persistent_adapter_key(limits: LaunchGeometryLimits) -> String {
585 let mut hasher = blake3::Hasher::new();
586 hasher.update(b"vyre-natural-launch-adapter-v2\0");
587 hasher.update(limits.backend.as_bytes());
588 hasher.update(&limits.max_threads_per_block.to_le_bytes());
589 for axis in limits.max_block_dim {
590 hasher.update(&axis.to_le_bytes());
591 }
592 for axis in limits.max_grid_dim {
593 hasher.update(&axis.to_le_bytes());
594 }
595 hasher.update(&limits.max_threads_per_sm.to_le_bytes());
596 let digest = hasher.finalize();
597 let mut key = String::with_capacity(92);
598 key.push_str("natural-launch-feedback-v2-");
599 crate::pipeline::hashing::push_lower_hex(digest.as_bytes(), &mut key);
600 key
601}
602
603fn valid_persisted_launch_selection(selected: [u32; 3], limits: LaunchGeometryLimits) -> bool {
604 selected[1] == 1 && selected[2] == 1 && candidate_x_fits_limits(selected[0], limits)
605}
606
607fn is_natural_gradient_launch_tunable(
608 program: &Program,
609 declared: [u32; 3],
610 element_count: u32,
611) -> bool {
612 declared[0] != 0
613 && declared[1] == 1
614 && declared[2] == 1
615 && element_count != 0
616 && program
617 .entry
618 .iter()
619 .any(|node| !matches!(node, Node::Return))
620 && !program.non_composable_with_self
621 && !program_uses_launch_geometry_ids(program)
622 && program
623 .buffers
624 .iter()
625 .all(|buffer| buffer.kind() != MemoryKind::Shared)
626}
627
628fn candidate_x_fits_limits(candidate_x: u32, limits: LaunchGeometryLimits) -> bool {
629 candidate_x != 0
630 && candidate_x <= limits.max_threads_per_block
631 && candidate_x <= limits.max_block_dim[0]
632}
633
634fn peak_resident_threads_per_compute_unit(
642 declared_x: u32,
643 limits: LaunchGeometryLimits,
644) -> Option<u32> {
645 WORKGROUP_CANDIDATES
646 .iter()
647 .copied()
648 .chain(std::iter::once(declared_x))
649 .filter(|&candidate_x| candidate_x_fits_limits(candidate_x, limits))
650 .filter_map(|candidate_x| limits.resident_threads_per_compute_unit(candidate_x))
651 .max()
652}
653
654fn cold_start_admits_width(
679 candidate_x: u32,
680 limits: LaunchGeometryLimits,
681 peak_resident: Option<u32>,
682) -> bool {
683 let Some(peak_resident) = peak_resident else {
684 return true;
685 };
686 limits
687 .resident_threads_per_compute_unit(candidate_x)
688 .is_none_or(|resident| resident >= peak_resident)
689}
690
691fn estimate_cold_start_latency_ns(element_count: u32, candidate_x: u32) -> u64 {
692 let groups = u64::from(element_count.div_ceil(candidate_x));
693 let scheduled_lanes = groups.saturating_mul(u64::from(candidate_x));
694 let idle_lanes = scheduled_lanes.saturating_sub(u64::from(element_count));
695 groups
696 .saturating_mul(COLD_START_GRID_STEP_NS)
697 .saturating_add(idle_lanes.saturating_mul(COLD_START_IDLE_LANE_NS))
698}
699
700#[must_use]
702pub fn program_vsa_fingerprint(program: &Program) -> Vec<u32> {
703 program_vsa_fingerprint_words(program).to_vec()
704}
705
706#[must_use]
708pub fn program_vsa_fingerprint_words(program: &Program) -> [u32; 8] {
709 let fingerprint = program.fingerprint();
710 let mut words = [0u32; 8];
711 for (word, chunk) in words.iter_mut().zip(fingerprint.chunks_exact(4)) {
712 *word = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
713 }
714 words
715}
716
717#[cfg(test)]
718mod tests {
719 use super::*;
720 use crate::binding::BindingRole;
721 use vyre_foundation::ir::{BufferDecl, DataType, Expr, Node, Program};
722
723 #[test]
724 fn program_vsa_fingerprint_words_match_wire_decoder() {
725 let program = Program::wrapped(vec![], [64, 1, 1], vec![]);
726 let words = program_vsa_fingerprint_words(&program);
727 let fingerprint = program.fingerprint();
728
729 for (index, chunk) in fingerprint.chunks_exact(4).enumerate() {
730 assert_eq!(
731 words[index],
732 u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])
733 );
734 }
735 assert_eq!(program_vsa_fingerprint(&program), words.to_vec());
736 }
737
738 #[test]
739 fn launch_plan_prepare_into_reuses_param_words() {
740 let program = Program::wrapped(vec![], [64, 1, 1], vec![]);
741 let bindings = vec![Binding {
742 name: std::sync::Arc::from("input"),
743 binding: 0,
744 buffer_index: 0,
745 role: BindingRole::Input,
746 element_size: 4,
747 preferred_alignment: 64,
748 element_count: 7,
749 static_byte_len: Some(28),
750 input_index: Some(0),
751 output_index: None,
752 }];
753 let limits = LaunchGeometryLimits {
754 backend: "test",
755 max_threads_per_block: 1024,
756 max_block_dim: [1024, 1024, 64],
757 max_grid_dim: [u32::MAX, u32::MAX, u32::MAX],
758 max_threads_per_sm: 1536,
759 };
760 let mut plan = LaunchPlan {
761 param_words: Vec::with_capacity(8),
762 ..LaunchPlan::new()
763 };
764 let ptr = plan.param_words.as_ptr();
765 plan.prepare_into(&program, &bindings, &DispatchConfig::default(), limits)
766 .unwrap();
767 assert_eq!(plan.element_count, 7);
768 assert_eq!(plan.grid, [1, 1, 1]);
769 assert_eq!(plan.param_words, vec![7, 7]);
770 assert_eq!(plan.max_binding_alignment, 64);
771 assert_eq!(plan.param_words.as_ptr(), ptr);
772 }
773
774 #[test]
775 fn natural_gradient_launch_tunes_safe_1d_storage_program() {
776 let program = Program::wrapped(
777 vec![BufferDecl::output("out", 0, DataType::U32).with_count(4096)],
778 [32, 1, 1],
779 vec![],
780 );
781 let bindings = vec![Binding {
782 name: std::sync::Arc::from("out"),
783 binding: 0,
784 buffer_index: 0,
785 role: BindingRole::Output,
786 element_size: 4,
787 preferred_alignment: 128,
788 element_count: 4096,
789 static_byte_len: Some(16_384),
790 input_index: None,
791 output_index: Some(0),
792 }];
793 let limits = LaunchGeometryLimits {
794 backend: "test",
795 max_threads_per_block: 1024,
796 max_block_dim: [1024, 1024, 64],
797 max_grid_dim: [u32::MAX, u32::MAX, u32::MAX],
798 max_threads_per_sm: 1536,
799 };
800 let mut plan = LaunchPlan::new();
801
802 plan.prepare_into_for_mode(
803 &program,
804 &bindings,
805 &DispatchConfig::default(),
806 limits,
807 Mode::NaturalGradient,
808 )
809 .expect("Fix: safe 1D storage launch should accept natural-gradient cold start");
810
811 assert_eq!(
812 plan.workgroup,
813 [512, 1, 1],
814 "Fix: cold start must pick the widest width that keeps every resident thread slot usable. Was [1024,1,1], which is 1536/1024 = 1 block per SM and 512 stranded slots on every SM."
815 );
816 assert_eq!(
817 limits.resident_threads_per_compute_unit(plan.workgroup[0]),
818 Some(1536),
819 "Fix: the chosen width must strand no per-SM thread slot when a candidate dividing 1536 evenly exists."
820 );
821 assert_eq!(plan.grid, [8, 1, 1]);
822 assert_eq!(plan.element_count, 4096);
823 }
824
825 #[test]
826 fn natural_gradient_launch_preserves_declared_shape_for_local_workgroup_ids() {
827 let program = Program::wrapped(
828 vec![BufferDecl::output("out_local_ids", 0, DataType::U32).with_count(4096)],
829 [1024, 1, 1],
830 vec![
831 Node::let_bind("lane", Expr::LocalId { axis: 0 }),
832 Node::let_bind("block", Expr::WorkgroupId { axis: 0 }),
833 Node::let_bind(
834 "global",
835 Expr::add(
836 Expr::mul(Expr::var("block"), Expr::u32(1024)),
837 Expr::var("lane"),
838 ),
839 ),
840 Node::store("out_local_ids", Expr::var("global"), Expr::var("lane")),
841 ],
842 );
843 let bindings = vec![Binding {
844 name: std::sync::Arc::from("out_local_ids"),
845 binding: 0,
846 buffer_index: 0,
847 role: BindingRole::Output,
848 element_size: 4,
849 preferred_alignment: 128,
850 element_count: 4096,
851 static_byte_len: Some(16_384),
852 input_index: None,
853 output_index: Some(0),
854 }];
855 let limits = LaunchGeometryLimits {
856 backend: "test",
857 max_threads_per_block: 1024,
858 max_block_dim: [1024, 1024, 64],
859 max_grid_dim: [u32::MAX, u32::MAX, u32::MAX],
860 max_threads_per_sm: 1536,
861 };
862
863 assert_eq!(
864 effective_launch_workgroup_for_mode(
865 &program,
866 &bindings,
867 &DispatchConfig::default(),
868 limits,
869 Mode::NaturalGradient,
870 ),
871 [1024, 1, 1],
872 "Fix: automatic launch tuning must not change kernels whose LocalId/WorkgroupId arithmetic makes workgroup shape semantic."
873 );
874 }
875
876 #[test]
877 fn measured_launch_feedback_overrides_heuristic_cold_start() {
878 let dir = tempfile::tempdir()
879 .expect("Fix: measured launch feedback test needs an isolated tuner cache");
880 let path = dir.path().join("launch-feedback.toml");
881 let program = Program::wrapped(
882 vec![BufferDecl::output("out_feedback_isolated", 0, DataType::U32).with_count(8192)],
883 [32, 1, 1],
884 vec![],
885 );
886 let config = DispatchConfig::default();
887 let limits = LaunchGeometryLimits {
888 backend: "test",
889 max_threads_per_block: 1024,
890 max_block_dim: [1024, 1024, 64],
891 max_grid_dim: [u32::MAX, u32::MAX, u32::MAX],
892 max_threads_per_sm: 1536,
893 };
894 let key = NaturalLaunchCacheKey::new(&program, [32, 1, 1], 8192, limits);
895 natural_launch_cache_remove(key);
896
897 assert_eq!(
898 natural_gradient_cold_start_workgroup_with_store(
899 &program,
900 [32, 1, 1],
901 8192,
902 limits,
903 Some(&path),
904 ),
905 Some([512, 1, 1]),
906 "Fix: this pins the cold-start selector's output, not a required constant. It was [1024,1,1] under a heuristic with no occupancy term at all, so the old message's claim of an occupancy-efficient shape described the opposite of what it selected."
907 );
908 assert!(
909 record_launch_measurement_for_mode_with_store(
910 &program,
911 &config,
912 limits,
913 8192,
914 [64, 1, 1],
915 1,
916 Mode::NaturalGradient,
917 Some(&path),
918 ),
919 "Fix: natural-gradient resolver must accept measured backend timing for safe 1D launches."
920 );
921 assert_eq!(
922 natural_gradient_cold_start_workgroup_with_store(
923 &program,
924 [32, 1, 1],
925 8192,
926 limits,
927 Some(&path),
928 ),
929 Some([64, 1, 1]),
930 "Fix: measured launch feedback must steer future automatic launch choices."
931 );
932 }
933
934 #[test]
935 fn persisted_launch_feedback_rehydrates_measured_selection() {
936 let dir = tempfile::tempdir()
937 .expect("Fix: launch feedback persistence test needs a temporary cache directory");
938 let path = dir.path().join("launch-feedback.toml");
939 let program = Program::wrapped(
940 vec![BufferDecl::output("out_persisted", 0, DataType::U32).with_count(16_384)],
941 [32, 1, 1],
942 vec![],
943 );
944 let limits = LaunchGeometryLimits {
945 backend: "test",
946 max_threads_per_block: 1024,
947 max_block_dim: [1024, 1024, 64],
948 max_grid_dim: [u32::MAX, u32::MAX, u32::MAX],
949 max_threads_per_sm: 1536,
950 };
951 let key = NaturalLaunchCacheKey::new(&program, [32, 1, 1], 16_384, limits);
952 natural_launch_cache_remove(key);
953
954 persist_natural_launch_selection_to_path(key, [64, 1, 1], &path)
955 .expect("Fix: measured launch feedback should persist through the tuner cache format");
956
957 assert_eq!(
958 natural_gradient_cold_start_workgroup_with_store(
959 &program,
960 [32, 1, 1],
961 16_384,
962 limits,
963 Some(&path),
964 ),
965 Some([64, 1, 1]),
966 "Fix: automatic launch resolution must rehydrate measured feedback from the bounded tuner cache before falling back to heuristics."
967 );
968 }
969
970 #[test]
971 fn natural_gradient_launch_preserves_explicit_and_shared_memory_shapes() {
972 let program = Program::wrapped(
973 vec![
974 BufferDecl::output("out", 0, DataType::U32).with_count(4096),
975 BufferDecl::workgroup("scratch", 64, DataType::U32),
976 ],
977 [64, 1, 1],
978 vec![],
979 );
980 let bindings = vec![Binding {
981 name: std::sync::Arc::from("out"),
982 binding: 0,
983 buffer_index: 0,
984 role: BindingRole::Output,
985 element_size: 4,
986 preferred_alignment: 128,
987 element_count: 4096,
988 static_byte_len: Some(16_384),
989 input_index: None,
990 output_index: Some(0),
991 }];
992 let limits = LaunchGeometryLimits {
993 backend: "test",
994 max_threads_per_block: 1024,
995 max_block_dim: [1024, 1024, 64],
996 max_grid_dim: [u32::MAX, u32::MAX, u32::MAX],
997 max_threads_per_sm: 1536,
998 };
999 let mut config = DispatchConfig::default();
1000 config.workgroup_override = Some([256, 1, 1]);
1001
1002 assert_eq!(
1003 effective_launch_workgroup_for_mode(
1004 &program,
1005 &bindings,
1006 &config,
1007 limits,
1008 Mode::NaturalGradient,
1009 ),
1010 [256, 1, 1],
1011 "Fix: explicit dispatch workgroup overrides must remain authoritative."
1012 );
1013
1014 let default_config = DispatchConfig::default();
1015 assert_eq!(
1016 effective_launch_workgroup_for_mode(
1017 &program,
1018 &bindings,
1019 &default_config,
1020 limits,
1021 Mode::NaturalGradient,
1022 ),
1023 [64, 1, 1],
1024 "Fix: workgroup-local scratch kernels must keep their declared shape."
1025 );
1026 }
1027
1028 #[test]
1034 fn natural_launch_cache_recovers_from_poisoned_mutex_without_silent_fallback() {
1035 let program = Program::wrapped(
1036 vec![BufferDecl::output("out_poison_test", 0, DataType::U32).with_count(2048)],
1037 [32, 1, 1],
1038 vec![],
1039 );
1040 let limits = LaunchGeometryLimits {
1041 backend: "test-poison",
1042 max_threads_per_block: 1024,
1043 max_block_dim: [1024, 1024, 64],
1044 max_grid_dim: [u32::MAX, u32::MAX, u32::MAX],
1045 max_threads_per_sm: 0,
1046 };
1047 let key = NaturalLaunchCacheKey::new(&program, [32, 1, 1], 2048, limits);
1048 natural_launch_cache_remove(key);
1049
1050 natural_launch_cache_set(
1052 key,
1053 NaturalLaunchEntry {
1054 selected: [128, 1, 1],
1055 measurements: BTreeMap::new(),
1056 },
1057 );
1058
1059 let result = natural_launch_cache_get(key);
1070 assert_eq!(
1071 result,
1072 Some([128, 1, 1]),
1073 "Fix: natural_launch_cache_get must return the stored selection [128, 1, 1], not None"
1074 );
1075
1076 let source = include_str!("launch.rs");
1078 let production = source
1079 .split("#[cfg(test)]")
1080 .next()
1081 .expect("Fix: production section must precede test section");
1082 assert!(
1083 !production.contains(".lock()\n .ok()") && !production.contains(".lock().ok()"),
1084 "Fix: natural_launch_cache functions must not use .lock().ok(), that silently swallows mutex poison"
1085 );
1086 }
1087
1088 #[test]
1096 fn record_launch_measurement_starts_fresh_only_when_no_prior_history_exists() {
1097 let dir = tempfile::tempdir()
1098 .expect("Fix: measurement history test needs a temporary cache directory");
1099 let path = dir.path().join("measurements-test.toml");
1100 let program = Program::wrapped(
1101 vec![BufferDecl::output("out_meas_history", 0, DataType::U32).with_count(4096)],
1102 [32, 1, 1],
1103 vec![],
1104 );
1105 let config = DispatchConfig::default();
1106 let limits = LaunchGeometryLimits {
1107 backend: "test-measurements",
1108 max_threads_per_block: 1024,
1109 max_block_dim: [1024, 1024, 64],
1110 max_grid_dim: [u32::MAX, u32::MAX, u32::MAX],
1111 max_threads_per_sm: 0,
1112 };
1113 let key = NaturalLaunchCacheKey::new(&program, [32, 1, 1], 4096, limits);
1114 natural_launch_cache_remove(key);
1115
1116 assert!(
1118 record_launch_measurement_for_mode_with_store(
1119 &program,
1120 &config,
1121 limits,
1122 4096,
1123 [256, 1, 1],
1124 100,
1125 Mode::NaturalGradient,
1126 Some(&path),
1127 ),
1128 "Fix: first measurement must be accepted into the cache"
1129 );
1130
1131 let after_first = natural_launch_cache_get(key);
1133 assert!(
1134 after_first.is_some(),
1135 "Fix: cache must hold a selection after the first measurement"
1136 );
1137
1138 assert!(
1141 record_launch_measurement_for_mode_with_store(
1142 &program,
1143 &config,
1144 limits,
1145 4096,
1146 [128, 1, 1],
1147 50,
1148 Mode::NaturalGradient,
1149 Some(&path),
1150 ),
1151 "Fix: second measurement must be accepted into the cache"
1152 );
1153
1154 let measurements = natural_launch_cache_measurements(key)
1155 .expect("Fix: cache must hold measurements after two records");
1156 assert!(
1157 measurements.len() >= 2,
1158 "Fix: measurement history must accumulate across calls, got {} entries, expected >= 2",
1159 measurements.len()
1160 );
1161 assert_eq!(
1162 measurements.get(&[256, 1, 1]),
1163 Some(&100),
1164 "Fix: first measurement (workgroup=[256,1,1], 100ns) must be retained in history"
1165 );
1166 assert_eq!(
1167 measurements.get(&[128, 1, 1]),
1168 Some(&50),
1169 "Fix: second measurement (workgroup=[128,1,1], 50ns) must be present in history"
1170 );
1171 }
1172
1173 const RTX_5090_SM_COUNT: u32 = 170;
1175
1176 fn blackwell_5090_limits() -> LaunchGeometryLimits {
1179 LaunchGeometryLimits {
1180 backend: "blackwell-5090-test",
1181 max_threads_per_block: 1024,
1182 max_block_dim: [1024, 1024, 64],
1183 max_grid_dim: [u32::MAX, u32::MAX, u32::MAX],
1184 max_threads_per_sm: 1536,
1185 }
1186 }
1187
1188 fn tunable_1d_program(output: &'static str, element_count: u32, declared: [u32; 3]) -> Program {
1195 Program::wrapped(
1196 vec![BufferDecl::output(output, 0, DataType::U32).with_count(element_count)],
1197 declared,
1198 vec![],
1199 )
1200 }
1201
1202 #[test]
1213 fn cold_start_never_strands_resident_thread_slots_when_an_even_divisor_exists() {
1214 let limits = blackwell_5090_limits();
1215 for (output, element_count) in [
1216 ("out_no_strand_1k", 1024u32),
1217 ("out_no_strand_4k", 4096),
1218 ("out_no_strand_64k", 65_536),
1219 ("out_no_strand_tail", 4097),
1220 ("out_no_strand_100k", 100_000),
1221 ] {
1222 let program = tunable_1d_program(output, element_count, [32, 1, 1]);
1223 let resolved = resolve_launch_workgroup_for_mode(
1224 &program,
1225 &DispatchConfig::default(),
1226 limits,
1227 element_count,
1228 Mode::NaturalGradient,
1229 );
1230 let resident = limits.resident_threads_per_compute_unit(resolved[0]);
1231 assert_eq!(
1232 resident,
1233 Some(1536),
1234 "Fix: cold start chose {resolved:?} for {element_count} elements, leaving {} of every SM's 1536 thread slots unusable. Prefer a width that divides the per-SM budget evenly.",
1235 1536 - resident.unwrap_or(1536)
1236 );
1237 }
1238 }
1239
1240 #[test]
1247 fn cold_start_still_admits_1024_where_the_per_sm_budget_divides_evenly() {
1248 let limits = LaunchGeometryLimits {
1249 backend: "even-divisor-test",
1250 max_threads_per_block: 1024,
1251 max_block_dim: [1024, 1024, 64],
1252 max_grid_dim: [u32::MAX, u32::MAX, u32::MAX],
1253 max_threads_per_sm: 2048,
1254 };
1255 let program = tunable_1d_program("out_even_divisor", 65_536, [32, 1, 1]);
1256
1257 assert_eq!(
1258 limits.resident_threads_per_compute_unit(1024),
1259 Some(2048),
1260 "Fix: 1024 must reach every thread slot on a 2048-thread SM, otherwise this test's premise is wrong."
1261 );
1262 assert_eq!(
1263 resolve_launch_workgroup_for_mode(
1264 &program,
1265 &DispatchConfig::default(),
1266 limits,
1267 65_536,
1268 Mode::NaturalGradient,
1269 ),
1270 [1024, 1, 1],
1271 "Fix: residency-aware cold start must stay a residency rule. A width that strands nothing has to remain selectable on every device."
1272 );
1273 }
1274
1275 #[test]
1286 fn unreported_per_sm_budget_leaves_cold_start_byte_identical() {
1287 let limits = LaunchGeometryLimits {
1288 backend: "unreported-residency-test",
1289 max_threads_per_block: 1024,
1290 max_block_dim: [1024, 1024, 64],
1291 max_grid_dim: [u32::MAX, u32::MAX, u32::MAX],
1292 max_threads_per_sm: 0,
1293 };
1294 assert_eq!(
1295 limits.resident_threads_per_compute_unit(1024),
1296 None,
1297 "Fix: an unreported per-SM budget must answer `unknown`, never a guessed number."
1298 );
1299
1300 for (output, element_count) in [
1301 ("out_inert_1k", 1024u32),
1302 ("out_inert_4k", 4096),
1303 ("out_inert_64k", 65_536),
1304 ("out_inert_1000", 1_000),
1305 ("out_inert_4097", 4097),
1306 ("out_inert_100k", 100_000),
1307 ] {
1308 let program = tunable_1d_program(output, element_count, [32, 1, 1]);
1309 assert_eq!(
1310 resolve_launch_workgroup_for_mode(
1311 &program,
1312 &DispatchConfig::default(),
1313 limits,
1314 element_count,
1315 Mode::NaturalGradient,
1316 ),
1317 [1024, 1, 1],
1318 "Fix: residency-aware cold start must be inert for a backend that reports no per-SM budget. {element_count} elements resolved differently than they did before residency entered this decision."
1319 );
1320 }
1321 }
1322
1323 #[test]
1330 fn explicit_geometry_pins_outrank_residency_aware_cold_start() {
1331 let limits = blackwell_5090_limits();
1332 let declared = [256, 1, 1];
1333 let program = tunable_1d_program("out_pinned_geometry", 262_144, declared);
1334
1335 let mut pinned_workgroup = DispatchConfig::default();
1336 pinned_workgroup.workgroup_override = Some([64, 1, 1]);
1337 assert_eq!(
1338 resolve_launch_workgroup_for_mode(
1339 &program,
1340 &pinned_workgroup,
1341 limits,
1342 262_144,
1343 Mode::NaturalGradient,
1344 ),
1345 [64, 1, 1],
1346 "Fix: an explicit workgroup override stays authoritative even when residency prefers another width."
1347 );
1348
1349 let mut pinned_grid = DispatchConfig::default();
1350 pinned_grid.grid_override = Some([1024, 1, 1]);
1351 assert_eq!(
1352 resolve_launch_workgroup_for_mode(
1353 &program,
1354 &pinned_grid,
1355 limits,
1356 262_144,
1357 Mode::NaturalGradient,
1358 ),
1359 declared,
1360 "Fix: an explicit grid override must keep the declared workgroup, since the caller sized the grid against it."
1361 );
1362 }
1363
1364 #[test]
1378 fn cooperative_lane_ceiling_follows_the_resolved_width_not_the_declared_one() {
1379 let limits = blackwell_5090_limits();
1380 let declared = [256, 1, 1];
1381 let program = tunable_1d_program("out_resolved_ceiling", 262_144, declared);
1382 let lane_ceiling = |width: u32| -> u64 {
1383 u64::from(
1384 limits
1385 .resident_threads_per_compute_unit(width)
1386 .expect("Fix: this device model reports a per-SM thread budget"),
1387 ) * u64::from(RTX_5090_SM_COUNT)
1388 };
1389
1390 let resolved = resolve_launch_workgroup_for_mode(
1391 &program,
1392 &DispatchConfig::default(),
1393 limits,
1394 262_144,
1395 Mode::NaturalGradient,
1396 );
1397 assert_ne!(
1398 resolved, declared,
1399 "Fix: this program is tunable, so a bound taken from the declared width would bound a width nothing launches."
1400 );
1401 assert_eq!(
1402 lane_ceiling(1024),
1403 174_080,
1404 "Fix: 1024 wide is 1 block/SM x 170 SMs x 1024 lanes. This is the ceiling the defect produced."
1405 );
1406 assert_eq!(
1407 lane_ceiling(resolved[0]),
1408 261_120,
1409 "Fix: the resolved width must reach the device's full cooperative capacity, 1536 resident threads x 170 SMs. Seeing 174,080 here means the tuner resolved 1024 again."
1410 );
1411
1412 let mut pinned = DispatchConfig::default();
1413 pinned.workgroup_override = Some(declared);
1414 assert_eq!(
1415 resolve_launch_workgroup_for_mode(
1416 &program,
1417 &pinned,
1418 limits,
1419 262_144,
1420 Mode::NaturalGradient,
1421 ),
1422 declared,
1423 "Fix: a pinned width must resolve to itself so the declared and resolved bounds coincide."
1424 );
1425 assert_eq!(lane_ceiling(declared[0]), 261_120);
1426 }
1427
1428 #[test]
1434 fn measured_feedback_can_still_select_a_width_cold_start_would_reject() {
1435 let dir =
1436 tempfile::tempdir().expect("Fix: measured feedback test needs an isolated tuner cache");
1437 let path = dir.path().join("residency-feedback.toml");
1438 let limits = blackwell_5090_limits();
1439 let declared = [32, 1, 1];
1440 let program = tunable_1d_program("out_measured_beats_residency", 65_536, declared);
1441 let key = NaturalLaunchCacheKey::new(&program, declared, 65_536, limits);
1442 natural_launch_cache_remove(key);
1443
1444 assert_eq!(
1445 natural_gradient_cold_start_workgroup_with_store(
1446 &program,
1447 declared,
1448 65_536,
1449 limits,
1450 Some(&path),
1451 ),
1452 Some([512, 1, 1]),
1453 "Fix: with no measurements the residency preference decides."
1454 );
1455 natural_launch_cache_remove(key);
1456 assert!(
1457 record_launch_measurement_for_mode_with_store(
1458 &program,
1459 &DispatchConfig::default(),
1460 limits,
1461 65_536,
1462 [1024, 1, 1],
1463 1,
1464 Mode::NaturalGradient,
1465 Some(&path),
1466 ),
1467 "Fix: a real timing for a residency-poor width must still be accepted."
1468 );
1469 assert_eq!(
1470 natural_gradient_cold_start_workgroup_with_store(
1471 &program,
1472 declared,
1473 65_536,
1474 limits,
1475 Some(&path),
1476 ),
1477 Some([1024, 1, 1]),
1478 "Fix: residency governs the cold start only. Measured feedback must remain able to choose a width cold start would never propose."
1479 );
1480 }
1481}