Skip to main content

vyre_driver/
launch.rs

1//! Backend-neutral dispatch launch preparation.
2
3use 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/// Fully prepared launch metadata shared by concrete drivers.
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct LaunchPlan {
32    /// Logical element count passed to the lowered kernel.
33    pub element_count: u32,
34    /// Effective workgroup/block shape after dispatch overrides.
35    pub workgroup: [u32; 3],
36    /// Effective grid shape after dispatch overrides or inference.
37    pub grid: [u32; 3],
38    /// Per-buffer element-count metadata uploaded as the shared params buffer.
39    pub param_words: Vec<u32>,
40    /// Maximum preferred alignment across all launch bindings.
41    ///
42    /// Concrete drivers use this to pick upload staging and device-buffer
43    /// allocation paths without re-inspecting Program buffer declarations.
44    pub max_binding_alignment: usize,
45}
46
47impl LaunchPlan {
48    /// Empty launch plan with reusable parameter-word storage.
49    #[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    /// Prepare dispatch geometry and parameter words from a validated binding plan.
61    ///
62    /// # Errors
63    ///
64    /// Returns when caller overrides produce zero dimensions, overflow the
65    /// logical launch element count, or exceed backend-reported launch limits.
66    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    /// Prepare dispatch geometry and parameter words, reusing this plan's buffers.
78    ///
79    /// # Errors
80    ///
81    /// Returns when caller overrides produce zero dimensions, overflow the
82    /// logical launch element count, or exceed backend-reported launch limits.
83    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                // Non-1D workgroups need an explicit grid_override  -
109                // there's no single right way to map an unknown
110                // element_count across N×M (or N×M×K) thread tiles,
111                // and silently picking one produces silently-wrong
112                // results. Force the caller to make the choice.
113                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/// Resolve the backend-visible workgroup shape for a dispatch.
194///
195/// Explicit caller overrides remain authoritative. When no override is
196/// supplied and `VYRE_AUTOTUNER` resolves to natural-gradient mode, eligible
197/// 1D storage-only kernels receive a deterministic natural-gradient cold-start
198/// workgroup before grid inference.
199#[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/// Resolve the backend-visible workgroup shape with an explicit tuner mode.
210///
211/// This is public so backends whose shader/pipeline compilation must include
212/// the selected workgroup size can derive the same shape before lowering.
213#[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/// Record a measured launch result for the natural-gradient launch resolver.
233///
234/// Backends should call this only after a real dispatch timing is available.
235/// The function returns `true` when the measurement was accepted into the
236/// bounded feedback cache. Explicit caller overrides, explicit grid launches,
237/// non-natural tuner modes, non-1D kernels, workgroup-local scratch kernels,
238/// zero timings, and out-of-limit candidates are ignored so measured feedback
239/// never changes kernel semantics.
240#[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        // v2 adds the per-compute-unit thread budget. It selects the width, so
475        // a v1 entry may record a choice made without it and must not be read
476        // back as if it had been.
477        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
634/// Highest resident thread count per compute unit that any admissible width
635/// reaches on this device.
636///
637/// `None` when the backend reports no per-unit thread budget. That is the
638/// inert case: no width is preferred over another on residency grounds and
639/// cold start selects exactly what it selected before residency entered this
640/// decision.
641fn 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
654/// Whether cold start may propose `candidate_x` with no measurement behind it.
655///
656/// Residency ranks ahead of the latency estimate because the estimate cannot
657/// see occupancy at all: it counts workgroups and idle tail lanes, so it always
658/// favours the widest candidate and the tail penalty vanishes entirely when the
659/// element count is a multiple of that width. Resident threads per unit is
660/// `(max_threads_per_sm / width) * width` with an integral division, so against
661/// a 1536-thread unit a 1024-wide group hosts one block and strands 512 slots,
662/// while 32 through 512 all host enough blocks to fill all 1536. Only widths
663/// tying for the peak survive here; the latency estimate then breaks that tie,
664/// and it breaks it toward the widest survivor.
665///
666/// Ranking widest-first is also what keeps the thread-only residency model
667/// sound. It ignores the device-reported cap on blocks per unit, so it
668/// overstates residency at the narrow end: measured on an RTX 5090, whose cap
669/// is 24, a 32-wide group gets 24 blocks and 768 resident threads where this
670/// model predicts 48 and 1536, a factor of two. Selecting the widest survivor
671/// never reaches that regime, and on this device the pick lands at 512 with 3
672/// blocks per SM, well clear of the cap. A future tie-break toward narrower
673/// widths would need the block cap as a second input.
674///
675/// This gate applies to cold start only. A width carrying a real measurement
676/// bypasses it entirely, so measured feedback can still choose a width cold
677/// start would never propose.
678fn 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/// Compute the shared VSA program fingerprint used by backend caches.
701#[must_use]
702pub fn program_vsa_fingerprint(program: &Program) -> Vec<u32> {
703    program_vsa_fingerprint_words(program).to_vec()
704}
705
706/// Compute the shared VSA program fingerprint without heap allocation.
707#[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    // Reproducing test for: launch-cache-mutex-poison-silent-fallback
1029    // Before fix: .lock().ok() silently returned None on mutex poison, causing a silent
1030    // fallback from feedback-informed to cold-start workgroup selection.
1031    // After fix: .unwrap_or_else(|p| p.into_inner()) recovers the guard and preserves
1032    // accumulated timing data even after a thread panics while holding the lock.
1033    #[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        // Write a known workgroup selection into the cache.
1051        natural_launch_cache_set(
1052            key,
1053            NaturalLaunchEntry {
1054                selected: [128, 1, 1],
1055                measurements: BTreeMap::new(),
1056            },
1057        );
1058
1059        // Poison the mutex by panicking inside a std::thread::scope closure while holding a
1060        // lock acquired via get_or_init. We do this by manually poisoning via std::panic.
1061        // Since NATURAL_LAUNCH_CACHE is a process-global OnceLock we simulate the recovery
1062        // path by verifying .unwrap_or_else(|p| p.into_inner()) in cache_get directly.
1063        // The key observable: cache_get must return the previously-written selection
1064        // (not None) even when poison recovery is required.
1065        //
1066        // We cannot poison the global mutex in a test without affecting parallel tests;
1067        // instead we verify the recovery API is correct: unwrap_or_else on a non-poisoned
1068        // mutex must return the same result as .unwrap(), proving the path is correct.
1069        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        // Also verify the source does not use .lock().ok() (the silencing pattern).
1077        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    // Reproducing test for: launch-cache-measurements-unwrap-or-default-silent-feedback-loss
1089    // Before fix: natural_launch_cache_measurements returned None on mutex poison and
1090    // record_launch_measurement_for_mode_with_store would .unwrap_or_default() that None,
1091    // overwriting all prior measurement history with a single-sample empty map.
1092    // After fix (driven by the mutex fix): None from cache_measurements means genuinely
1093    // no prior entry, not a poison-induced data loss. The measurement path correctly starts
1094    // from an empty map only when no prior measurements exist.
1095    #[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        // First measurement accepted (starts from empty, correct).
1117        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        // Read back the selection (must be [256, 1, 1] (only candidate with real timing)).
1132        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        // Second measurement with a *faster* timing for a different candidate.
1139        // The history from the first must be preserved (not replaced by an empty map).
1140        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    /// Streaming multiprocessors on the RTX 5090 this defect was measured on.
1174    const RTX_5090_SM_COUNT: u32 = 170;
1175
1176    /// Launch limits shaped like that RTX 5090: 1024 threads per block and a
1177    /// 1536-thread per-SM residency budget, which 1024 does not divide.
1178    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    /// A 1-D storage-only program the natural-gradient resolver treats as
1189    /// tunable: no `LocalId`/`WorkgroupId` arithmetic, no workgroup scratch,
1190    /// and composable with itself, so none of the early-out gates fire.
1191    ///
1192    /// Each caller passes a distinct output name because the resolver memoizes
1193    /// on the program fingerprint.
1194    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    /// Cold start must never choose a width that strands per-SM thread slots
1203    /// while a width dividing the budget evenly is available.
1204    ///
1205    /// Blocks per SM is an integral division, so on a 1536-thread SM a
1206    /// 1024-wide group hosts exactly one block and leaves 512 of every SM's
1207    /// 1536 slots unusable, a third of the device idle by arithmetic. Every
1208    /// candidate from 32 to 512 divides 1536 exactly. The element counts below
1209    /// span both sides of the old estimate's blind spot: multiples of 1024,
1210    /// where its idle-lane penalty vanishes and the widest candidate won
1211    /// outright, and counts with a tail.
1212    #[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    /// The fix is a residency rule, not a hardcoded rejection of 1024.
1241    ///
1242    /// On a device whose per-SM budget is 2048 threads, 1024 hosts two whole
1243    /// blocks and reaches every slot, so it ties with every narrower candidate
1244    /// on residency and the latency estimate breaks the tie toward the widest.
1245    /// A fix that simply banned 1024 would fail here.
1246    #[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    /// A backend reporting no per-SM thread budget keeps its previous cold
1276    /// start bit for bit.
1277    ///
1278    /// WebGPU exposes no such number, so wgpu reports `max_threads_per_sm: 0`.
1279    /// Zero must make the residency preference inert rather than derive an
1280    /// opinion from a budget the backend never supplied: every candidate stays
1281    /// eligible and the latency estimate alone decides, which is [1024,1,1] for
1282    /// each count below. Multiples of 1024 are the interesting ones, because
1283    /// that is where the estimate's idle-lane penalty vanishes entirely. If
1284    /// someone later makes 0 mean "guess a budget", this fails loudly.
1285    #[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    /// Both explicit geometry pins outrank residency-aware cold start.
1324    ///
1325    /// `workgroup_override` is authoritative and `grid_override` returns the
1326    /// declared shape, because a caller that pinned its geometry is telling the
1327    /// driver the shape is load bearing. `exatok` sets both, which is why this
1328    /// defect never reached it.
1329    #[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    /// The cooperative residency bound follows the width the tuner RESOLVES,
1365    /// never the width the program DECLARES.
1366    ///
1367    /// This is the test whose failure exposed the defect and it must stay. A
1368    /// preflight boundary written against a declared 256 expected the flip at
1369    /// 1021 blocks (1020 = 6 blocks/SM x 170 SMs is the last grid that fits)
1370    /// and observed it at 681, because the tuner had silently resolved 1024:
1371    /// 681 x 256 = 174,336 lanes, exactly one lane past 170 x 1024 = 174,080.
1372    /// Earlier over-residency tests missed this because 1024 blocks of 256
1373    /// exceeds the ceiling under BOTH widths, so they were green for the wrong
1374    /// reason. Past the ceiling a grid-sync program stops fitting one
1375    /// cooperative launch and takes the host split route, so a width choice
1376    /// turns one launch into many.
1377    #[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    /// Measured feedback still outranks the residency preference.
1429    ///
1430    /// The residency rule governs the choice made with no measurements. Once a
1431    /// real timing says 1024 is faster for a given program, the tuner must be
1432    /// free to take it even though cold start would never have proposed it.
1433    #[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}