1use crate::cuda::Device;
7use crate::plan::{ArgSpec, BenchPlan, Candidate};
8use crate::stats::{Summary, summarize};
9use serde::{Deserialize, Serialize};
10use std::path::Path;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::time::Instant;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct CandidateResult {
16 pub id: String,
17 pub config: String,
18 pub status: String, #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub error: Option<String>,
21 pub warmup: u32,
22 pub repeats: u32,
23 #[serde(default)]
24 pub times_ms: Vec<f64>,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub summary: Option<Summary>,
27 pub gpu_seconds: f64,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct Results {
33 pub schema: String,
34 pub kernel: String,
35 pub entry: String,
36 pub plan_cc: String,
37 pub device_name: String,
38 pub device_cc: String,
39 pub driver_version: String,
40 pub candidates: Vec<CandidateResult>,
41 pub total_gpu_seconds: f64,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub strategy: Option<String>,
45 #[serde(default)]
47 pub budget_exhausted: bool,
48}
49
50pub struct RunOptions {
51 pub order: Vec<usize>,
54 pub budget_secs: Option<f64>,
56 pub strategy: Option<String>,
58}
59
60impl RunOptions {
61 pub fn exhaustive(plan: &BenchPlan) -> Self {
62 RunOptions {
63 order: (0..plan.candidates.len()).collect(),
64 budget_secs: None,
65 strategy: Some("exhaustive".into()),
66 }
67 }
68}
69
70pub fn run_plan(
73 plan: &BenchPlan,
74 plan_dir: &Path,
75 results_path: &Path,
76 options: &RunOptions,
77 progress: &mut dyn FnMut(&str),
78) -> Result<Results, String> {
79 let device = Device::open()?;
80 progress(&format!(
81 "device: {} (cc {}, driver {})",
82 device.name, device.cc, device.driver_version
83 ));
84 if device.cc != plan.cc {
85 progress(&format!(
86 "WARNING: plan was gated at cc {}, device is cc {} — verdicts do not transfer \
87 across parts (docs/SAFETY.md); results will be labelled with the device cc",
88 plan.cc, device.cc
89 ));
90 }
91
92 let mut results = match Results::load(results_path) {
93 Some(existing) if existing.schema == "results.v1" => {
94 progress(&format!(
95 "resuming: {} candidates already measured",
96 existing.candidates.len()
97 ));
98 existing
99 }
100 _ => Results {
101 schema: "results.v1".into(),
102 kernel: plan.kernel.clone(),
103 entry: plan.entry.clone(),
104 plan_cc: plan.cc.clone(),
105 device_name: device.name.clone(),
106 device_cc: device.cc.clone(),
107 driver_version: device.driver_version.clone(),
108 candidates: Vec::new(),
109 total_gpu_seconds: 0.0,
110 strategy: options.strategy.clone(),
111 budget_exhausted: false,
112 },
113 };
114 results.budget_exhausted = false;
115
116 let heartbeat_stop = start_heartbeat();
117 let sweep_started = Instant::now();
118
119 for &index in &options.order {
120 let Some(candidate) = plan.candidates.get(index) else {
121 return Err(format!("order index {index} out of range"));
122 };
123 if results.candidates.iter().any(|c| c.id == candidate.id) {
124 continue;
125 }
126 if let Some(budget) = options.budget_secs
127 && sweep_started.elapsed().as_secs_f64() >= budget
128 {
129 results.budget_exhausted = true;
130 progress(&format!(
131 "budget exhausted after {:.1}s: {} of {} candidates measured (resumable)",
132 sweep_started.elapsed().as_secs_f64(),
133 results.candidates.len(),
134 plan.candidates.len()
135 ));
136 break;
137 }
138 let watchdog = if candidate.unsafe_candidate {
143 results.candidates.push(CandidateResult {
144 id: candidate.id.clone(),
145 config: candidate.config.clone(),
146 status: "timeout".into(),
147 error: Some(format!(
148 "unsafe candidate did not complete within {UNSAFE_TIMEOUT_SECS}s; presumed hung (this is the failure mode the gate predicts)"
149 )),
150 warmup: candidate.warmup,
151 repeats: candidate.repeats,
152 times_ms: Vec::new(),
153 summary: None,
154 gpu_seconds: unsafe_timeout_secs() as f64,
155 });
156 results.checkpoint(results_path)?;
157 progress(&format!(
158 "{} UNSAFE candidate: watchdog armed at {}s",
159 candidate.id,
160 unsafe_timeout_secs()
161 ));
162 Some(arm_watchdog(unsafe_timeout_secs()))
163 } else {
164 None
165 };
166 let started = Instant::now();
167 let outcome = run_candidate(&device, plan, plan_dir, candidate);
168 if let Some(armed) = watchdog {
169 armed.store(true, Ordering::Relaxed); results.candidates.retain(|c| c.id != candidate.id);
172 }
173 let gpu_seconds = started.elapsed().as_secs_f64();
174 let result = match outcome {
175 Ok(times_ms) => {
176 let summary = summarize(×_ms);
177 progress(&format!(
178 "{} {}: median {} over {} repeats ({:.1}s)",
179 candidate.id,
180 candidate.config,
181 summary
182 .as_ref()
183 .map(|s| format!(
184 "{:.4} ms [{:.4}, {:.4}]",
185 s.median_ms, s.ci95_lo_ms, s.ci95_hi_ms
186 ))
187 .unwrap_or_else(|| "n/a".into()),
188 candidate.repeats,
189 gpu_seconds,
190 ));
191 CandidateResult {
192 id: candidate.id.clone(),
193 config: candidate.config.clone(),
194 status: "ok".into(),
195 error: None,
196 warmup: candidate.warmup,
197 repeats: candidate.repeats,
198 times_ms,
199 summary,
200 gpu_seconds,
201 }
202 }
203 Err(e) => {
204 progress(&format!("{} ERROR: {e}", candidate.id));
205 CandidateResult {
206 id: candidate.id.clone(),
207 config: candidate.config.clone(),
208 status: "error".into(),
209 error: Some(e),
210 warmup: candidate.warmup,
211 repeats: candidate.repeats,
212 times_ms: Vec::new(),
213 summary: None,
214 gpu_seconds,
215 }
216 }
217 };
218 results.candidates.push(result);
219 results.total_gpu_seconds = sweep_started.elapsed().as_secs_f64();
220 results.checkpoint(results_path)?;
221 }
222
223 heartbeat_stop.store(true, Ordering::Relaxed);
224 results.total_gpu_seconds = sweep_started.elapsed().as_secs_f64();
225 results.checkpoint(results_path)?;
226 Ok(results)
227}
228
229fn run_candidate(
230 device: &Device,
231 plan: &BenchPlan,
232 plan_dir: &Path,
233 candidate: &Candidate,
234) -> Result<Vec<f64>, String> {
235 let ptx_path = plan_dir.join(&candidate.ptx);
236 let ptx = std::fs::read_to_string(&ptx_path)
237 .map_err(|e| format!("reading {}: {e}", ptx_path.display()))?;
238 let module = device.load_module(&ptx, &plan.entry)?;
239
240 let mut buffers = Vec::new(); for (i, arg) in candidate.args.iter().enumerate() {
244 match arg {
245 ArgSpec::InF32 { len } => {
246 let host: Vec<f32> = deterministic_f32(*len);
247 let buf = device.alloc(host.len() * 4)?;
248 device.copy_in(&buf, cast_bytes(&host))?;
249 buffers.push((i, buf));
250 }
251 ArgSpec::InU32 { len, modulo } => {
252 let host: Vec<u32> = deterministic_u32(*len, *modulo);
253 let buf = device.alloc(host.len() * 4)?;
254 device.copy_in(&buf, cast_bytes(&host))?;
255 buffers.push((i, buf));
256 }
257 ArgSpec::OutF32 { len } | ArgSpec::OutU32 { len } => {
258 let zero = vec![0u8; (*len as usize) * 4];
259 let buf = device.alloc(zero.len())?;
260 device.copy_in(&buf, &zero)?;
261 buffers.push((i, buf));
262 }
263 ArgSpec::LenOf { .. } | ArgSpec::U32 { .. } | ArgSpec::U64 { .. } => {}
264 }
265 }
266
267 let mut ptr_slots: Vec<u64> = Vec::new();
270 let mut u32_slots: Vec<u32> = Vec::new();
271 let mut u64_slots: Vec<u64> = Vec::new();
272 #[derive(Clone, Copy)]
273 enum Slot {
274 Ptr(usize),
275 U32(usize),
276 U64(usize),
277 }
278 let mut slots = Vec::with_capacity(candidate.args.len());
279 for (i, arg) in candidate.args.iter().enumerate() {
280 match arg {
281 ArgSpec::InF32 { .. }
282 | ArgSpec::InU32 { .. }
283 | ArgSpec::OutF32 { .. }
284 | ArgSpec::OutU32 { .. } => {
285 let buf = &buffers
286 .iter()
287 .find(|(idx, _)| *idx == i)
288 .expect("buffer materialized")
289 .1;
290 ptr_slots.push(buf.ptr);
291 slots.push(Slot::Ptr(ptr_slots.len() - 1));
292 }
293 ArgSpec::LenOf { of } => {
294 let len = match candidate.args.get(*of) {
295 Some(ArgSpec::InF32 { len })
296 | Some(ArgSpec::OutF32 { len })
297 | Some(ArgSpec::OutU32 { len })
298 | Some(ArgSpec::InU32 { len, .. }) => *len,
299 other => return Err(format!("len_of {of} points at {other:?}")),
300 };
301 u64_slots.push(len);
302 slots.push(Slot::U64(u64_slots.len() - 1));
303 }
304 ArgSpec::U32 { value } => {
305 u32_slots.push(*value as u32);
306 slots.push(Slot::U32(u32_slots.len() - 1));
307 }
308 ArgSpec::U64 { value } => {
309 u64_slots.push(*value);
310 slots.push(Slot::U64(u64_slots.len() - 1));
311 }
312 }
313 }
314 let mut params: Vec<*mut std::ffi::c_void> = slots
315 .iter()
316 .map(|slot| match slot {
317 Slot::Ptr(k) => std::ptr::from_mut(&mut ptr_slots[*k]).cast(),
318 Slot::U32(k) => std::ptr::from_mut(&mut u32_slots[*k]).cast(),
319 Slot::U64(k) => std::ptr::from_mut(&mut u64_slots[*k]).cast(),
320 })
321 .collect();
322
323 for _ in 0..candidate.warmup {
324 device.timed_launch(&module, candidate.grid, candidate.block, &mut params)?;
325 }
326 device.synchronize()?;
327
328 let mut times = Vec::with_capacity(candidate.repeats as usize);
329 for _ in 0..candidate.repeats {
330 times.push(device.timed_launch(&module, candidate.grid, candidate.block, &mut params)?);
331 }
332 device.synchronize()?;
333 Ok(times)
334}
335
336fn cast_bytes<T>(data: &[T]) -> &[u8] {
337 unsafe { std::slice::from_raw_parts(data.as_ptr().cast(), std::mem::size_of_val(data)) }
338}
339
340fn deterministic_f32(len: u64) -> Vec<f32> {
342 let mut state = 0x9e3779b97f4a7c15u64;
343 (0..len)
344 .map(|_| {
345 state ^= state << 13;
346 state ^= state >> 7;
347 state ^= state << 17;
348 ((state >> 40) as f32) / ((1u64 << 24) as f32)
349 })
350 .collect()
351}
352
353fn deterministic_u32(len: u64, modulo: u64) -> Vec<u32> {
354 let modulo = modulo.max(1);
355 let mut state = 0x2545f4914f6cdd1du64;
356 (0..len)
357 .map(|_| {
358 state ^= state << 13;
359 state ^= state >> 7;
360 state ^= state << 17;
361 (state % modulo) as u32
362 })
363 .collect()
364}
365
366impl Results {
367 pub fn load(path: &Path) -> Option<Self> {
368 let text = std::fs::read_to_string(path).ok()?;
369 serde_json::from_str(&text).ok()
370 }
371
372 pub fn checkpoint(&self, path: &Path) -> Result<(), String> {
374 let tmp = path.with_extension("json.tmp");
375 std::fs::write(
376 &tmp,
377 serde_json::to_string_pretty(self).expect("results serialize"),
378 )
379 .map_err(|e| e.to_string())?;
380 std::fs::rename(&tmp, path).map_err(|e| e.to_string())
381 }
382}
383
384const UNSAFE_TIMEOUT_SECS: u64 = 10;
385
386fn unsafe_timeout_secs() -> u64 {
387 std::env::var("LAUNCHBOUND_UNSAFE_TIMEOUT_SECS")
388 .ok()
389 .and_then(|v| v.parse().ok())
390 .unwrap_or(UNSAFE_TIMEOUT_SECS)
391}
392
393fn arm_watchdog(deadline_secs: u64) -> std::sync::Arc<AtomicBool> {
397 let disarmed = std::sync::Arc::new(AtomicBool::new(false));
398 let flag = disarmed.clone();
399 std::thread::spawn(move || {
400 let start = Instant::now();
401 while start.elapsed().as_secs() < deadline_secs {
402 if flag.load(Ordering::Relaxed) {
403 return;
404 }
405 std::thread::sleep(std::time::Duration::from_millis(100));
406 }
407 if !flag.load(Ordering::Relaxed) {
408 eprintln!(
409 "watchdog: unsafe candidate exceeded {deadline_secs}s; exiting so the checkpointed timeout record stands (exit 3, rerun to continue)"
410 );
411 std::process::exit(3);
412 }
413 });
414 disarmed
415}
416
417fn start_heartbeat() -> &'static AtomicBool {
421 static STOP: AtomicBool = AtomicBool::new(false);
422 STOP.store(false, Ordering::Relaxed);
423 let duty: u64 = std::env::var("LAUNCHBOUND_HEARTBEAT_PCT")
424 .ok()
425 .and_then(|v| v.parse().ok())
426 .unwrap_or(40)
427 .clamp(1, 100);
428 std::thread::spawn(move || {
429 let mut sink = 0u64;
430 while !STOP.load(Ordering::Relaxed) {
431 let spin = Instant::now();
432 while spin.elapsed().as_millis() < duty as u128 {
433 sink = sink.wrapping_mul(6364136223846793005).wrapping_add(1);
434 }
435 std::hint::black_box(sink);
436 std::thread::sleep(std::time::Duration::from_millis(100 - duty.min(99)));
437 }
438 });
439 &STOP
440}