1use std::time::{Duration, Instant};
13
14use henad_compute::entry::ModelState;
15use henad_compute::fault::{Fault, STEPPING, catching};
16use henad_compute::gpu::fault::catching_on;
17use henad_compute::gpu::{GpuContext, GpuSimState, stepping};
18use henad_compute::simulation::RunSetup;
19use henad_core::action::{Fire, RefusedActions};
20use henad_core::metadata::{Backend, Structure};
21use henad_core::model::SimState;
22use henad_core::params::{ParamDescriptor, ParamValue};
23
24pub const BENCH_FIRE: Fire = Fire::BeforeStep;
26
27#[derive(Debug, Clone)]
29#[non_exhaustive]
30pub struct BenchmarkSettings {
31 pub setup: RunSetup,
33 pub steps: u64,
35 pub warmup: u64,
41 pub global_warmup: u64,
43 pub repetitions: u64,
45}
46
47impl BenchmarkSettings {
48 pub fn new(setup: RunSetup, steps: u64) -> Self {
50 Self {
51 setup,
52 steps,
53 warmup: 0,
54 global_warmup: 0,
55 repetitions: 1,
56 }
57 }
58}
59
60#[derive(Debug)]
62#[non_exhaustive]
63pub enum BenchmarkEvent<'a> {
64 Started {
66 backend: Backend,
68 parallel_jobs: Option<usize>,
70 },
71 GlobalWarmupStarted,
73 GlobalWarmupFinished(Duration),
75 RepetitionStarted(u64),
77 RepetitionFinished(&'a RepetitionReport),
79}
80
81#[derive(Debug, Clone)]
83#[non_exhaustive]
84pub struct RepetitionReport {
85 pub index: u64,
87 pub seed: Option<u64>,
89 pub elapsed: Duration,
91 pub population_after_warmup: u64,
94 pub population_after_steps: u64,
96 pub heap_bytes: Option<usize>,
98}
99
100#[derive(Debug, Clone)]
102#[non_exhaustive]
103pub struct BenchmarkReport {
104 pub repetitions: Vec<RepetitionReport>,
106 pub parallel_jobs: Option<usize>,
108 pub grid_size: Option<(u32, u32)>,
112}
113
114pub fn run_benchmark(
129 settings: &BenchmarkSettings,
130 gpu: Option<&GpuContext>,
131 on_event: &mut dyn FnMut(BenchmarkEvent<'_>),
132) -> Result<BenchmarkReport, Fault> {
133 let setup = &settings.setup;
134 let entry = setup.entry();
135 let (backend, parallel_jobs) = match (entry.metadata().backend, gpu) {
136 (Backend::Gpu, Some(_)) => (Backend::Gpu, None),
138 _ => match entry.build(setup.values(), setup.seed(), gpu)? {
139 ModelState::Gpu(_) => (Backend::Gpu, None),
140 ModelState::Cpu(state) => (Backend::Cpu, state.parallel_jobs()),
141 },
142 };
143 on_event(BenchmarkEvent::Started { backend, parallel_jobs });
144
145 if settings.global_warmup > 0 {
146 on_event(BenchmarkEvent::GlobalWarmupStarted);
147 let elapsed = match entry.build(setup.values(), setup.seed(), gpu)? {
148 ModelState::Cpu(mut state) => catching(STEPPING, || {
149 let start = Instant::now();
150 rayon::scope(|_| {
151 for _ in 0..settings.global_warmup {
152 state.step();
153 }
154 });
155 start.elapsed()
156 })?,
157 ModelState::Gpu(mut state) => {
158 let ctx = device(gpu)?;
159 catching_on(ctx, STEPPING, || {
160 let start = Instant::now();
161 stepping::run_steps(&mut *state, ctx, settings.global_warmup)?;
162 Ok(start.elapsed())
163 })??
164 }
165 };
166 on_event(BenchmarkEvent::GlobalWarmupFinished(elapsed));
167 }
168
169 let mut repetitions = Vec::new();
172 let mut grid_size = None;
173 for index in 0..settings.repetitions {
174 on_event(BenchmarkEvent::RepetitionStarted(index));
175 let seed = setup.seed().map(|seed| seed.wrapping_add(index));
176 let report = match entry.build(setup.values(), seed, gpu)? {
177 ModelState::Cpu(mut state) => {
178 let (report, grid) = catching(STEPPING, || cpu_repetition(&mut *state, settings, index, seed))??;
179 grid_size = grid;
180 report
181 }
182 ModelState::Gpu(mut state) => {
183 let ctx = device(gpu)?;
184 catching_on(ctx, STEPPING, || {
185 gpu_repetition(&mut *state, ctx, settings, index, seed)
186 })??
187 }
188 };
189 on_event(BenchmarkEvent::RepetitionFinished(&report));
190 repetitions.push(report);
191 }
192 if matches!(entry.metadata().structure, Structure::GpuGrid { .. }) {
193 grid_size = grid_size_from_params(entry.param_descriptors(), setup.values());
194 }
195 Ok(BenchmarkReport {
196 repetitions,
197 parallel_jobs,
198 grid_size,
199 })
200}
201
202fn device(gpu: Option<&GpuContext>) -> Result<&GpuContext, Fault> {
204 gpu.ok_or_else(|| Fault::refused(STEPPING, "a GPU model was built with no device"))
205}
206
207fn cpu_repetition(
211 state: &mut dyn SimState,
212 settings: &BenchmarkSettings,
213 index: u64,
214 seed: Option<u64>,
215) -> Result<(RepetitionReport, Option<(u32, u32)>), Fault> {
216 let schedule = settings.setup.schedule();
217 let mut refusals = Refusals::default();
218 rayon::scope(|_| {
221 for _ in 0..settings.warmup {
222 refusals.note(&schedule.run_due(state));
223 state.step();
224 }
225 });
226 let population_after_warmup = state.population();
229 let heap_bytes = state.heap_bytes();
230 let grid = state.grid_view().map(|grid| (grid.width, grid.height));
231
232 let start = Instant::now();
233 rayon::scope(|_| {
234 for _ in 0..settings.steps {
235 refusals.note(&schedule.run_due(state));
236 state.step();
237 }
238 });
239 let elapsed = start.elapsed();
240 refusals.note(&schedule.run_due(state));
242 refusals.check()?;
243 let report = RepetitionReport {
244 index,
245 seed,
246 elapsed,
247 population_after_warmup,
248 population_after_steps: state.population(),
249 heap_bytes: Some(heap_bytes),
250 };
251 Ok((report, grid))
252}
253
254fn gpu_repetition(
260 state: &mut dyn GpuSimState,
261 ctx: &GpuContext,
262 settings: &BenchmarkSettings,
263 index: u64,
264 seed: Option<u64>,
265) -> Result<RepetitionReport, Fault> {
266 let schedule = settings.setup.schedule();
267 let mut refusals = Refusals::default();
268 refusals.note(&stepping::run_steps_acting(
269 state,
270 ctx,
271 settings.warmup,
272 schedule,
273 BENCH_FIRE,
274 )?);
275 let population_after_warmup = state.population();
276 ctx.queue.submit([]);
278 stepping::wait(ctx)?;
279
280 let start = Instant::now();
281 let refused = stepping::run_steps_acting(state, ctx, settings.steps, schedule, BENCH_FIRE)?;
282 let elapsed = start.elapsed();
283 refusals.note(&refused);
284 refusals.note(&stepping::run_due(state, ctx, schedule));
285 stepping::wait(ctx)?;
286 refusals.check()?;
287 Ok(RepetitionReport {
288 index,
289 seed,
290 elapsed,
291 population_after_warmup,
292 population_after_steps: state.population(),
293 heap_bytes: None,
294 })
295}
296
297#[derive(Default)]
302struct Refusals {
303 first: Option<(String, u64)>,
304}
305
306impl Refusals {
307 #[inline]
308 fn note(&mut self, refused: &RefusedActions<'_>) {
309 if let Some(action) = refused.first()
310 && self.first.is_none()
311 {
312 self.first = Some((action.id.clone(), action.tick));
313 }
314 }
315
316 fn check(self) -> Result<(), Fault> {
317 match self.first {
318 None => Ok(()),
319 Some((id, tick)) => Err(Fault::refused(
320 STEPPING,
321 format!("the model refused its own action '{id}' at tick {tick}"),
322 )),
323 }
324 }
325}
326
327fn grid_size_from_params(descriptors: &[ParamDescriptor], values: &[ParamValue]) -> Option<(u32, u32)> {
333 let find = |id: &str| -> Option<u32> {
334 let index = descriptors.iter().position(|descriptor| descriptor.id == id)?;
335 match values.get(index) {
336 Some(ParamValue::U32(value)) => Some(*value),
337 _ => None,
338 }
339 };
340 Some((find("grid_width")?, find("grid_height")?))
341}
342
343#[cfg(test)]
344mod tests {
345 use std::panic::AssertUnwindSafe;
346 use std::sync::atomic::{AtomicUsize, Ordering};
347 use std::sync::{Arc, Mutex};
348
349 use henad_compute::entry::{ModelState, register_grid_model};
350 use henad_compute::gpu::{GpuContext, stepping};
351 use henad_core::action::{ActionDescriptor, Fire, Schedule, Scheduled};
352 use henad_core::authoring::model::grid_model::GridModel;
353 use henad_core::grid::Grid2D;
354 use henad_core::metadata::Backend;
355 use henad_core::params::{ParamDescriptor, ParamValue};
356 use henad_core::topology::NeighborhoodKind;
357 use henad_core::view::{StatDescriptor, StatValue};
358
359 use super::{BENCH_FIRE, BenchmarkEvent, BenchmarkSettings, gpu_repetition, run_benchmark};
360 use crate::tests::support::{entry, headless_device};
361
362 static SEEN: Mutex<Vec<u64>> = Mutex::new(Vec::new());
364
365 struct Counter;
367
368 impl GridModel for Counter {
369 const NAME: &'static str = "Counter";
370 const ID: &'static str = "counter";
371 const DESCRIPTION: &'static str = "A model that records the tick its action sees, registered only by tests";
372 const PALETTE: &'static [[u8; 4]] = &[[0, 0, 0, 0xFF]];
373 const NEIGHBORHOOD: NeighborhoodKind = NeighborhoodKind::Moore;
374 const STATS: &'static [StatDescriptor] = &[StatDescriptor::new("Count", [0xFF, 0xFF, 0xFF, 0xFF])];
375 const ACTIONS: &'static [ActionDescriptor] = &[ActionDescriptor::new("record", "Record")];
376 type Params = ();
377
378 fn param_descriptors() -> Vec<ParamDescriptor> {
379 Vec::new()
380 }
381
382 fn from_params(_params: &[ParamValue]) {}
383
384 fn init(grid: &mut Grid2D<u8>, _params: &[ParamValue], _rng: &mut u64) {
385 grid.current_mut().fill(0);
386 }
387
388 fn step_cell(cell: u8, _neighbors: &[u8], _params: &(), _rng: &mut u64) -> u8 {
389 cell.wrapping_add(1)
390 }
391
392 fn act(_action: usize, grid: &mut Grid2D<u8>, _params: &[ParamValue], _rng: &mut u64) {
393 SEEN.lock()
394 .expect("no test panicked holding it")
395 .push(u64::from(grid.current()[0]));
396 }
397
398 fn stats(grid: &Grid2D<u8>) -> Vec<StatValue> {
399 vec![StatValue::Scalar(f64::from(grid.current()[0]))]
400 }
401 }
402
403 #[test]
408 fn a_benchmark_fires_each_action_before_its_step() {
409 let counter = register_grid_model::<Counter>();
410 let mut setup = counter
411 .setup()
412 .set("grid_width", 4u32)
413 .and_then(|setup| setup.set("grid_height", 4u32))
414 .expect("the grid fits")
415 .with_seed(40);
416 for tick in [0, 2, 3, 5, 9] {
417 setup = setup.act_at("record", tick).expect("the model declares it");
418 }
419 let mut settings = BenchmarkSettings::new(setup, 3);
420 settings.warmup = 2;
421 settings.global_warmup = 4;
422 settings.repetitions = 3;
423 SEEN.lock().expect("no test panicked holding it").clear();
424 let mut events = Vec::new();
425 let report = run_benchmark(&settings, None, &mut |event| {
426 events.push(match event {
427 BenchmarkEvent::Started { .. } => "started".to_owned(),
428 BenchmarkEvent::GlobalWarmupStarted => "warming up".to_owned(),
429 BenchmarkEvent::GlobalWarmupFinished(_) => "warmed up".to_owned(),
430 BenchmarkEvent::RepetitionStarted(index) => format!("rep {index} starts"),
431 BenchmarkEvent::RepetitionFinished(repetition) => format!("rep {}", repetition.index),
432 });
433 })
434 .expect("the benchmark runs");
435
436 let seen = SEEN.lock().expect("no test panicked holding it").clone();
437 assert_eq!(
438 seen,
439 [0, 2, 3, 5].repeat(3),
440 "warm-up fires 0, the timed steps 2 and 3, the end 5"
441 );
442 assert_eq!(
443 events,
444 [
445 "started",
446 "warming up",
447 "warmed up",
448 "rep 0 starts",
449 "rep 0",
450 "rep 1 starts",
451 "rep 1",
452 "rep 2 starts",
453 "rep 2"
454 ]
455 );
456 let seeds: Vec<Option<u64>> = report.repetitions.iter().map(|repetition| repetition.seed).collect();
457 assert_eq!(seeds, [Some(40), Some(41), Some(42)], "repetition i takes base + i");
458 assert_eq!(report.grid_size, Some((4, 4)));
459
460 let mut default_seed = BenchmarkSettings::new(counter.setup(), 1);
461 default_seed.repetitions = 2;
462 let report = run_benchmark(&default_seed, None, &mut |_| {}).expect("the benchmark runs");
463 assert!(report.repetitions.iter().all(|repetition| repetition.seed.is_none()));
464 }
465
466 #[test]
469 fn a_benchmark_of_endless_repetitions_reports_the_first() {
470 let setup = register_grid_model::<Counter>()
471 .setup()
472 .set("grid_width", 4u32)
473 .and_then(|setup| setup.set("grid_height", 4u32))
474 .expect("the grid fits");
475 let mut settings = BenchmarkSettings::new(setup, 1);
476 settings.repetitions = 1 << 56;
477 let mut reported = 0;
478 let interrupted = std::panic::catch_unwind(AssertUnwindSafe(|| {
479 run_benchmark(&settings, None, &mut |event| {
480 if let BenchmarkEvent::RepetitionFinished(_) = event {
481 reported += 1;
482 panic!("the host interrupts the benchmark");
483 }
484 })
485 }));
486 assert!(interrupted.is_err());
487 assert_eq!(reported, 1);
488 }
489
490 #[test]
493 fn a_gpu_benchmark_builds_no_probe() {
494 let Some(ctx) = headless_device() else {
495 return;
496 };
497 let builds = Arc::new(AtomicUsize::new(0));
498 let counted = Arc::clone(&builds);
499 let sir = entry("gpu_sir", Some(&ctx)).wrap_factory(move |create| {
500 Arc::new(
501 move |params: &[ParamValue], seed: Option<u64>, gpu: Option<&GpuContext>| {
502 counted.fetch_add(1, Ordering::Relaxed);
503 create(params, seed, gpu)
504 },
505 )
506 });
507 let setup = sir
508 .setup()
509 .set("grid_width", 32u32)
510 .and_then(|setup| setup.set("grid_height", 32u32))
511 .expect("the grid fits");
512 let mut settings = BenchmarkSettings::new(setup, 2);
513 settings.repetitions = 2;
514 let mut started = Vec::new();
515 let mut on_event = |event: BenchmarkEvent<'_>| {
516 if let BenchmarkEvent::Started { backend, parallel_jobs } = event {
517 started.push((backend, parallel_jobs));
518 }
519 };
520 let report = run_benchmark(&settings, Some(&ctx), &mut on_event).expect("the benchmark runs");
521 assert_eq!(builds.load(Ordering::Relaxed), 2, "one build per repetition");
522 assert_eq!(report.grid_size, Some((32, 32)));
523 assert!(run_benchmark(&settings, None, &mut on_event).is_err());
524 assert_eq!(started, [(Backend::Gpu, None)], "the run with no device never starts");
525 }
526
527 fn schedule_at(ticks: &[u64]) -> Schedule {
528 let entries = ticks
529 .iter()
530 .map(|&tick| Scheduled {
531 index: 0,
532 id: "act".to_owned(),
533 tick,
534 })
535 .collect();
536 Schedule::from_entries(entries)
537 }
538
539 fn fire_runs(schedule: &Schedule, counts: &[u64], fire: Fire) -> Vec<Vec<u64>> {
541 let mut start = 0;
542 counts
543 .iter()
544 .map(|&count| {
545 let fired = schedule.fire_ticks(start, count, fire);
546 start += count;
547 fired
548 })
549 .collect()
550 }
551
552 #[test]
558 fn a_timed_run_fires_the_ticks_the_cpu_loop_times() {
559 let schedule = schedule_at(&(0..=8).collect::<Vec<u64>>());
560 for warmup in 0..4 {
561 for steps in 0..4 {
562 let runs = fire_runs(&schedule, &[warmup, steps], BENCH_FIRE);
563 let expected: [Vec<u64>; 2] = [(0..warmup).collect(), (warmup..warmup + steps).collect()];
564 assert_eq!(runs, expected, "--warmup {warmup} --steps {steps}");
565 }
566 }
567 }
568
569 #[test]
574 fn a_gpu_benchmark_repetition_fires_every_action_once() {
575 let Some(ctx) = headless_device() else {
576 return;
577 };
578 let sir = entry("gpu_sir", Some(&ctx));
579 let mut setup = sir
580 .setup()
581 .set("grid_width", 64u32)
582 .and_then(|setup| setup.set("grid_height", 64u32))
583 .expect("the grid fits")
584 .with_seed(1);
585 for tick in [0, 2, 3, 5] {
586 setup = setup.act_at("seed_outbreak", tick).expect("gpu_sir declares it");
587 }
588 let counts = |entries: &[henad_core::view::StatEntry]| -> Vec<f64> {
589 entries.iter().map(|entry| entry.value.scalar()).collect()
590 };
591 for (warmup, steps) in [(0, 0), (0, 3), (2, 0), (2, 3)] {
592 let mut settings = BenchmarkSettings::new(setup.clone(), steps);
593 settings.warmup = warmup;
594 let Ok(ModelState::Gpu(mut state)) = sir.build(setup.values(), setup.seed(), Some(&ctx)) else {
595 panic!("gpu_sir builds as a GPU model");
596 };
597 gpu_repetition(&mut *state, &ctx, &settings, 0, setup.seed()).expect("a repetition");
598 let after_repetition = counts(&stepping::sample_stats(&mut *state, &ctx).expect("a sample"));
599 let mut simulation = setup.build(Some(&ctx)).expect("gpu_sir builds");
600 simulation.run_to(warmup + steps).expect("gpu_sir steps");
601 let stepped = counts(simulation.stats().expect("a sample").entries());
602 assert_eq!(after_repetition, stepped, "--warmup {warmup} --steps {steps}");
603 }
604 }
605}