vyre_driver/persistent_kernel_policy.rs
1//! D1 substrate: persistent-kernel-mode decision policy.
2//!
3//! When a workload submits many small kernel launches with the same
4//! pipeline, the launch overhead dominates execution time (~5 µs
5//! per native launch, ~10–50 µs per portable queue submit). Replacing the
6//! N launches with ONE persistent kernel that polls a device-side
7//! work queue eliminates the per-launch cost entirely - a 100×
8//! speedup on workloads where kernel duration < 50 µs.
9//!
10//! Persistent mode has a one-time setup cost (allocate the work queue,
11//! launch the persistent kernel, signal shutdown at the end). This
12//! amortises only when the batch is large enough. The decision policy
13//! here owns the threshold: given the measured per-launch overhead and
14//! per-item kernel duration, should the dispatcher run N standard
15//! launches or one persistent kernel?
16//!
17//! Pure decision - no kernel launch, no Program walk. Caller passes
18//! the measurements; the substrate produces a verdict.
19
20/// Inputs to the persistent-kernel decision.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct PersistentKernelInputs {
23 /// Number of small launches in the upcoming batch.
24 pub batch_size: u32,
25 /// Average per-launch host-side overhead in nanoseconds. Measured
26 /// on the live backend at startup; native is typically ~5_000 ns,
27 /// portable typically ~25_000 ns.
28 pub per_launch_overhead_ns: u64,
29 /// Average per-item kernel duration in nanoseconds. The
30 /// dispatcher measures this on the warmup pass before the batch.
31 pub per_item_kernel_ns: u64,
32 /// Setup cost of bringing up persistent mode (work-queue alloc,
33 /// initial launch, signal handshake) in nanoseconds. native: ~50_000
34 /// for a fresh queue; portable: ~200_000.
35 pub persistent_setup_overhead_ns: u64,
36}
37
38/// Verdict returned by [`decide_persistent_kernel`].
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum PersistentKernelDecision {
41 /// Use the standard launch path - N separate kernel launches.
42 /// Either the batch is too small to amortise persistent setup,
43 /// or the per-item kernel is large enough that launch overhead
44 /// is negligible.
45 StandardLaunches,
46 /// Use persistent kernel mode - one launch + device-side queue
47 /// polling for `batch_size` work items.
48 PersistentKernel {
49 /// Predicted total time saved (in nanoseconds) by using the
50 /// persistent path vs N standard launches. Useful for
51 /// telemetry and for the autotune store.
52 savings_ns: u128,
53 },
54}
55
56/// Decide whether to use persistent kernel mode for this batch.
57///
58/// Standard launches cost: `batch_size * per_launch_overhead + batch_size * per_item_kernel`.
59/// Persistent cost: `persistent_setup + batch_size * per_item_kernel`.
60/// Persistent wins iff `batch_size * per_launch_overhead > persistent_setup`.
61///
62/// Returns `StandardLaunches` when batch_size is 0 or 1 (persistent
63/// mode never wins for a single launch - the setup cost dominates).
64#[must_use]
65pub fn decide_persistent_kernel(inputs: PersistentKernelInputs) -> PersistentKernelDecision {
66 if inputs.batch_size <= 1 {
67 return PersistentKernelDecision::StandardLaunches;
68 }
69 // Defensive: zero per-launch overhead means we have no model to
70 // amortise - keep the standard path.
71 if inputs.per_launch_overhead_ns == 0 {
72 return PersistentKernelDecision::StandardLaunches;
73 }
74 let standard_overhead =
75 u128::from(inputs.batch_size) * u128::from(inputs.per_launch_overhead_ns);
76 let persistent_setup_overhead_ns = u128::from(inputs.persistent_setup_overhead_ns);
77 if standard_overhead <= persistent_setup_overhead_ns {
78 return PersistentKernelDecision::StandardLaunches;
79 }
80 let savings_ns = standard_overhead - persistent_setup_overhead_ns;
81 PersistentKernelDecision::PersistentKernel { savings_ns }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 fn inp(batch: u32, launch_ovh: u64, item_ns: u64, setup: u64) -> PersistentKernelInputs {
89 PersistentKernelInputs {
90 batch_size: batch,
91 per_launch_overhead_ns: launch_ovh,
92 per_item_kernel_ns: item_ns,
93 persistent_setup_overhead_ns: setup,
94 }
95 }
96
97 #[test]
98 fn single_launch_is_always_standard() {
99 // No matter how cheap the persistent setup, a 1-launch batch
100 // can't beat the standard path.
101 let dec = decide_persistent_kernel(inp(1, 5_000, 1_000, 1_000));
102 assert_eq!(dec, PersistentKernelDecision::StandardLaunches);
103 }
104
105 #[test]
106 fn zero_batch_is_standard() {
107 let dec = decide_persistent_kernel(inp(0, 5_000, 1_000, 50_000));
108 assert_eq!(dec, PersistentKernelDecision::StandardLaunches);
109 }
110
111 #[test]
112 fn small_batch_below_amortisation_threshold_is_standard() {
113 // 5 launches × 5 µs = 25 µs total; persistent setup = 50 µs →
114 // standard is cheaper.
115 let dec = decide_persistent_kernel(inp(5, 5_000, 1_000, 50_000));
116 assert_eq!(dec, PersistentKernelDecision::StandardLaunches);
117 }
118
119 #[test]
120 fn batch_at_amortisation_threshold_is_standard() {
121 // Exactly equal - the policy uses strict `>` so equal cost
122 // stays on the standard path (cheaper to keep launching).
123 let dec = decide_persistent_kernel(inp(10, 5_000, 1_000, 50_000));
124 assert_eq!(dec, PersistentKernelDecision::StandardLaunches);
125 }
126
127 #[test]
128 fn large_batch_above_threshold_picks_persistent() {
129 // 100 launches × 5 µs = 500 µs; persistent setup = 50 µs →
130 // savings = 450 µs.
131 let dec = decide_persistent_kernel(inp(100, 5_000, 1_000, 50_000));
132 assert_eq!(
133 dec,
134 PersistentKernelDecision::PersistentKernel {
135 savings_ns: 450_000
136 }
137 );
138 }
139
140 #[test]
141 fn portable_typical_overheads_pick_persistent_at_modest_batch() {
142 // portable submit overhead ~25 µs; persistent setup ~200 µs.
143 // 10 launches × 25 µs = 250 µs > 200 µs setup → persistent.
144 let dec = decide_persistent_kernel(inp(10, 25_000, 5_000, 200_000));
145 assert_eq!(
146 dec,
147 PersistentKernelDecision::PersistentKernel { savings_ns: 50_000 }
148 );
149 }
150
151 #[test]
152 fn zero_per_launch_overhead_returns_standard() {
153 // Defensive: a backend that reports zero launch overhead has
154 // no model to amortise - keep the standard path.
155 let dec = decide_persistent_kernel(inp(1000, 0, 100, 50_000));
156 assert_eq!(dec, PersistentKernelDecision::StandardLaunches);
157 }
158
159 #[test]
160 fn savings_is_strictly_positive_for_persistent_verdict() {
161 let dec = decide_persistent_kernel(inp(1000, 5_000, 1_000, 50_000));
162 match dec {
163 PersistentKernelDecision::PersistentKernel { savings_ns } => {
164 assert!(savings_ns > 0);
165 }
166 other => panic!("expected PersistentKernel; got {:?}", other),
167 }
168 }
169
170 #[test]
171 fn item_duration_does_not_affect_decision() {
172 // The decision is purely about overhead vs setup; per-item
173 // kernel duration appears on both sides of the inequality
174 // and cancels.
175 let small_kernel = decide_persistent_kernel(inp(100, 5_000, 100, 50_000));
176 let large_kernel = decide_persistent_kernel(inp(100, 5_000, 1_000_000, 50_000));
177 assert_eq!(small_kernel, large_kernel);
178 }
179
180 #[test]
181 fn widened_arithmetic_preserves_extreme_savings() {
182 // Adversarial: batch_size × per_launch_overhead near u64::MAX
183 // must not panic or clamp the predicted savings.
184 let dec = decide_persistent_kernel(inp(u32::MAX, u64::MAX / 2, 1, 50_000));
185 match dec {
186 PersistentKernelDecision::PersistentKernel { savings_ns } => {
187 assert_eq!(
188 savings_ns,
189 u128::from(u32::MAX) * u128::from(u64::MAX / 2) - 50_000
190 );
191 }
192 other => panic!("expected PersistentKernel; got {:?}", other),
193 }
194 }
195}