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 existing = Results::load(results_path)?;
96 let mut results = match existing {
97 Some(existing) => {
98 progress(&format!(
99 "resuming: {} candidates already measured",
100 existing.candidates.len()
101 ));
102 existing
103 }
104 None => Results {
105 schema: "results.v1".into(),
106 kernel: plan.kernel.clone(),
107 entry: plan.entry.clone(),
108 plan_cc: plan.cc.clone(),
109 device_name: device.name.clone(),
110 device_cc: device.cc.clone(),
111 driver_version: device.driver_version.clone(),
112 candidates: Vec::new(),
113 total_gpu_seconds: 0.0,
114 strategy: options.strategy.clone(),
115 budget_exhausted: false,
116 },
117 };
118 results.budget_exhausted = false;
119
120 let heartbeat_stop = start_heartbeat();
121 let sweep_started = Instant::now();
122
123 for &index in &options.order {
124 let Some(candidate) = plan.candidates.get(index) else {
125 return Err(format!("order index {index} out of range"));
126 };
127 if results.candidates.iter().any(|c| c.id == candidate.id) {
128 continue;
129 }
130 if let Some(budget) = options.budget_secs
131 && sweep_started.elapsed().as_secs_f64() >= budget
132 {
133 results.budget_exhausted = true;
134 progress(&format!(
135 "budget exhausted after {:.1}s: {} of {} candidates measured (resumable)",
136 sweep_started.elapsed().as_secs_f64(),
137 results.candidates.len(),
138 plan.candidates.len()
139 ));
140 break;
141 }
142 let watchdog = if candidate.unsafe_candidate {
147 results.candidates.push(CandidateResult {
148 id: candidate.id.clone(),
149 config: candidate.config.clone(),
150 status: "timeout".into(),
151 error: Some(format!(
152 "unsafe candidate did not complete within {UNSAFE_TIMEOUT_SECS}s; presumed hung (this is the failure mode the gate predicts)"
153 )),
154 warmup: candidate.warmup,
155 repeats: candidate.repeats,
156 times_ms: Vec::new(),
157 summary: None,
158 gpu_seconds: unsafe_timeout_secs() as f64,
159 });
160 results.checkpoint(results_path)?;
161 progress(&format!(
162 "{} UNSAFE candidate: watchdog armed at {}s",
163 candidate.id,
164 unsafe_timeout_secs()
165 ));
166 Some(arm_watchdog(unsafe_timeout_secs()))
167 } else {
168 None
169 };
170 let started = Instant::now();
171 let outcome = run_candidate(&device, plan, plan_dir, candidate);
172 if let Some(armed) = watchdog {
173 armed.store(true, Ordering::Relaxed); results.candidates.retain(|c| c.id != candidate.id);
176 }
177 let gpu_seconds = started.elapsed().as_secs_f64();
178 let result = match outcome {
179 Ok(times_ms) => {
180 let summary = summarize(×_ms);
181 progress(&format!(
182 "{} {}: median {} over {} repeats ({:.1}s)",
183 candidate.id,
184 candidate.config,
185 summary
186 .as_ref()
187 .map(|s| format!(
188 "{:.4} ms [{:.4}, {:.4}]",
189 s.median_ms, s.ci95_lo_ms, s.ci95_hi_ms
190 ))
191 .unwrap_or_else(|| "n/a".into()),
192 candidate.repeats,
193 gpu_seconds,
194 ));
195 CandidateResult {
196 id: candidate.id.clone(),
197 config: candidate.config.clone(),
198 status: "ok".into(),
199 error: None,
200 warmup: candidate.warmup,
201 repeats: candidate.repeats,
202 times_ms,
203 summary,
204 gpu_seconds,
205 }
206 }
207 Err(e) => {
208 progress(&format!("{} ERROR: {e}", candidate.id));
209 CandidateResult {
210 id: candidate.id.clone(),
211 config: candidate.config.clone(),
212 status: "error".into(),
213 error: Some(e),
214 warmup: candidate.warmup,
215 repeats: candidate.repeats,
216 times_ms: Vec::new(),
217 summary: None,
218 gpu_seconds,
219 }
220 }
221 };
222 results.candidates.push(result);
223 results.total_gpu_seconds = sweep_started.elapsed().as_secs_f64();
224 results.checkpoint(results_path)?;
225 }
226
227 heartbeat_stop.store(true, Ordering::Relaxed);
228 results.total_gpu_seconds = sweep_started.elapsed().as_secs_f64();
229 results.checkpoint(results_path)?;
230 Ok(results)
231}
232
233fn run_candidate(
234 device: &Device,
235 plan: &BenchPlan,
236 plan_dir: &Path,
237 candidate: &Candidate,
238) -> Result<Vec<f64>, String> {
239 let ptx_path = plan_dir.join(&candidate.ptx);
240 let ptx = std::fs::read_to_string(&ptx_path)
241 .map_err(|e| format!("reading {}: {e}", ptx_path.display()))?;
242 let module = device.load_module(&ptx, &plan.entry)?;
243
244 let mut buffers = Vec::new(); for (i, arg) in candidate.args.iter().enumerate() {
248 match arg {
249 ArgSpec::InF32 { len } => {
250 let host: Vec<f32> = deterministic_f32(*len);
251 let buf = device.alloc(host.len() * 4)?;
252 device.copy_in(&buf, cast_bytes(&host))?;
253 buffers.push((i, buf));
254 }
255 ArgSpec::InU32 { len, modulo } => {
256 let host: Vec<u32> = deterministic_u32(*len, *modulo);
257 let buf = device.alloc(host.len() * 4)?;
258 device.copy_in(&buf, cast_bytes(&host))?;
259 buffers.push((i, buf));
260 }
261 ArgSpec::OutF32 { len } | ArgSpec::OutU32 { len } => {
262 let zero = vec![0u8; (*len as usize) * 4];
263 let buf = device.alloc(zero.len())?;
264 device.copy_in(&buf, &zero)?;
265 buffers.push((i, buf));
266 }
267 ArgSpec::LenOf { .. } | ArgSpec::U32 { .. } | ArgSpec::U64 { .. } => {}
268 }
269 }
270
271 let mut ptr_slots: Vec<u64> = Vec::new();
274 let mut u32_slots: Vec<u32> = Vec::new();
275 let mut u64_slots: Vec<u64> = Vec::new();
276 #[derive(Clone, Copy)]
277 enum Slot {
278 Ptr(usize),
279 U32(usize),
280 U64(usize),
281 }
282 let mut slots = Vec::with_capacity(candidate.args.len());
283 for (i, arg) in candidate.args.iter().enumerate() {
284 match arg {
285 ArgSpec::InF32 { .. }
286 | ArgSpec::InU32 { .. }
287 | ArgSpec::OutF32 { .. }
288 | ArgSpec::OutU32 { .. } => {
289 let buf = &buffers
290 .iter()
291 .find(|(idx, _)| *idx == i)
292 .expect("buffer materialized")
293 .1;
294 ptr_slots.push(buf.ptr);
295 slots.push(Slot::Ptr(ptr_slots.len() - 1));
296 }
297 ArgSpec::LenOf { of } => {
298 let len = match candidate.args.get(*of) {
299 Some(ArgSpec::InF32 { len })
300 | Some(ArgSpec::OutF32 { len })
301 | Some(ArgSpec::OutU32 { len })
302 | Some(ArgSpec::InU32 { len, .. }) => *len,
303 other => return Err(format!("len_of {of} points at {other:?}")),
304 };
305 u64_slots.push(len);
306 slots.push(Slot::U64(u64_slots.len() - 1));
307 }
308 ArgSpec::U32 { value } => {
309 u32_slots.push(*value as u32);
310 slots.push(Slot::U32(u32_slots.len() - 1));
311 }
312 ArgSpec::U64 { value } => {
313 u64_slots.push(*value);
314 slots.push(Slot::U64(u64_slots.len() - 1));
315 }
316 }
317 }
318 let mut params: Vec<*mut std::ffi::c_void> = slots
319 .iter()
320 .map(|slot| match slot {
321 Slot::Ptr(k) => std::ptr::from_mut(&mut ptr_slots[*k]).cast(),
322 Slot::U32(k) => std::ptr::from_mut(&mut u32_slots[*k]).cast(),
323 Slot::U64(k) => std::ptr::from_mut(&mut u64_slots[*k]).cast(),
324 })
325 .collect();
326
327 for _ in 0..candidate.warmup {
328 device.timed_launch(&module, candidate.grid, candidate.block, &mut params)?;
329 }
330 device.synchronize()?;
331
332 let mut times = Vec::with_capacity(candidate.repeats as usize);
333 for _ in 0..candidate.repeats {
334 times.push(device.timed_launch(&module, candidate.grid, candidate.block, &mut params)?);
335 }
336 device.synchronize()?;
337 Ok(times)
338}
339
340fn cast_bytes<T>(data: &[T]) -> &[u8] {
341 unsafe { std::slice::from_raw_parts(data.as_ptr().cast(), std::mem::size_of_val(data)) }
342}
343
344fn deterministic_f32(len: u64) -> Vec<f32> {
346 let mut state = 0x9e3779b97f4a7c15u64;
347 (0..len)
348 .map(|_| {
349 state ^= state << 13;
350 state ^= state >> 7;
351 state ^= state << 17;
352 ((state >> 40) as f32) / ((1u64 << 24) as f32)
353 })
354 .collect()
355}
356
357fn deterministic_u32(len: u64, modulo: u64) -> Vec<u32> {
358 let modulo = modulo.max(1);
359 let mut state = 0x2545f4914f6cdd1du64;
360 (0..len)
361 .map(|_| {
362 state ^= state << 13;
363 state ^= state >> 7;
364 state ^= state << 17;
365 (state % modulo) as u32
366 })
367 .collect()
368}
369
370impl Results {
371 pub fn load(path: &Path) -> Result<Option<Self>, String> {
394 let text = match std::fs::read_to_string(path) {
395 Ok(text) => text,
396 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
397 Err(e) => return Err(format!("{}: {e}", path.display())),
398 };
399 let value: serde_json::Value = serde_json::from_str(&text)
402 .map_err(|e| format!("{}: not JSON: {e}", path.display()))?;
403 let declared = value.get("schema").and_then(|s| s.as_str());
404 match declared {
405 Some("results.v1") => {}
406 Some(other) => {
407 return Err(format!(
408 "{}: unsupported results schema `{other}` (expected `results.v1`)",
409 path.display()
410 ));
411 }
412 None => {
413 return Err(format!(
414 "{}: not a results.v1 document (no `schema` field)",
415 path.display()
416 ));
417 }
418 }
419 serde_json::from_str(&text)
420 .map(Some)
421 .map_err(|e| format!("{}: not a results.v1 document: {e}", path.display()))
422 }
423
424 pub fn checkpoint(&self, path: &Path) -> Result<(), String> {
426 let tmp = path.with_extension("json.tmp");
427 std::fs::write(
428 &tmp,
429 serde_json::to_string_pretty(self).expect("results serialize"),
430 )
431 .map_err(|e| e.to_string())?;
432 std::fs::rename(&tmp, path).map_err(|e| e.to_string())
433 }
434}
435
436const UNSAFE_TIMEOUT_SECS: u64 = 10;
437
438fn unsafe_timeout_secs() -> u64 {
439 std::env::var("LAUNCHBOUND_UNSAFE_TIMEOUT_SECS")
440 .ok()
441 .and_then(|v| v.parse().ok())
442 .unwrap_or(UNSAFE_TIMEOUT_SECS)
443}
444
445fn arm_watchdog(deadline_secs: u64) -> std::sync::Arc<AtomicBool> {
449 let disarmed = std::sync::Arc::new(AtomicBool::new(false));
450 let flag = disarmed.clone();
451 std::thread::spawn(move || {
452 let start = Instant::now();
453 while start.elapsed().as_secs() < deadline_secs {
454 if flag.load(Ordering::Relaxed) {
455 return;
456 }
457 std::thread::sleep(std::time::Duration::from_millis(100));
458 }
459 if !flag.load(Ordering::Relaxed) {
460 eprintln!(
461 "watchdog: unsafe candidate exceeded {deadline_secs}s; exiting so the checkpointed timeout record stands (exit 3, rerun to continue)"
462 );
463 std::process::exit(3);
464 }
465 });
466 disarmed
467}
468
469fn start_heartbeat() -> &'static AtomicBool {
473 static STOP: AtomicBool = AtomicBool::new(false);
474 STOP.store(false, Ordering::Relaxed);
475 let duty: u64 = std::env::var("LAUNCHBOUND_HEARTBEAT_PCT")
476 .ok()
477 .and_then(|v| v.parse().ok())
478 .unwrap_or(40)
479 .clamp(1, 100);
480 std::thread::spawn(move || {
481 let mut sink = 0u64;
482 while !STOP.load(Ordering::Relaxed) {
483 let spin = Instant::now();
484 while spin.elapsed().as_millis() < duty as u128 {
485 sink = sink.wrapping_mul(6364136223846793005).wrapping_add(1);
486 }
487 std::hint::black_box(sink);
488 std::thread::sleep(std::time::Duration::from_millis(100 - duty.min(99)));
489 }
490 });
491 &STOP
492}