1use super::{OpUnderTest, Output};
10pub use ferrum_bench_core::release_regression::numerics::{
11 compare_outputs, NumericalFailure, NumericalMetrics, RawOutput,
12};
13use ferrum_bench_core::release_regression::numerics::{
14 validate_reference as valid_reference, validate_tolerance as valid_tolerance,
15};
16use serde::{Deserialize, Serialize};
17use std::any::Any;
18use std::panic::{catch_unwind, AssertUnwindSafe};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum RequiredBackend {
23 Metal,
24 Cuda,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum RequiredStatus {
30 Passed,
31 Failed,
32 NotRun,
33}
34
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct RequiredReport {
38 pub schema_version: u32,
39 pub op: String,
40 pub backend: RequiredBackend,
41 pub seed: u64,
42 pub tolerance: Option<f64>,
45 pub tolerance_f64_bits: u64,
46 pub status: RequiredStatus,
47 pub reason: Option<String>,
48 pub reference: Option<RawOutput>,
49 pub actual: Option<RawOutput>,
50 pub metrics: Option<NumericalMetrics>,
51}
52
53impl RequiredReport {
54 pub fn is_passed(&self) -> bool {
58 self.status == RequiredStatus::Passed
59 }
60
61 fn failed(&mut self, reason: impl Into<String>) {
62 self.status = RequiredStatus::Failed;
63 self.reason = Some(reason.into());
64 }
65}
66
67#[derive(Debug)]
68enum Availability {
69 #[cfg_attr(
70 not(any(test, feature = "cuda", all(target_os = "macos", feature = "metal"))),
71 allow(dead_code)
72 )]
73 Available,
74 Unavailable(String),
75}
76
77fn availability(backend: RequiredBackend) -> Result<Availability, String> {
78 match backend {
79 RequiredBackend::Metal => {
80 #[cfg(all(target_os = "macos", feature = "metal"))]
81 {
82 if ferrum_kernels::attention::metal::is_available() {
83 Ok(Availability::Available)
84 } else {
85 Ok(Availability::Unavailable(
86 "no Metal device is available".into(),
87 ))
88 }
89 }
90 #[cfg(not(all(target_os = "macos", feature = "metal")))]
91 {
92 Ok(Availability::Unavailable(
93 "Metal backend is not compiled for this platform".into(),
94 ))
95 }
96 }
97 RequiredBackend::Cuda => {
98 #[cfg(feature = "cuda")]
99 {
100 match ferrum_kernels::cuda_device_count()? {
101 0 => Ok(Availability::Unavailable(
102 "no CUDA device is available".into(),
103 )),
104 _ => Ok(Availability::Available),
105 }
106 }
107 #[cfg(not(feature = "cuda"))]
108 {
109 Ok(Availability::Unavailable(
110 "CUDA backend is not compiled".into(),
111 ))
112 }
113 }
114 }
115}
116
117fn backend_output(op: &dyn OpUnderTest, backend: RequiredBackend, seed: u64) -> Option<Output> {
118 let _ = (op, seed);
121 match backend {
122 RequiredBackend::Metal => {
123 #[cfg(all(target_os = "macos", feature = "metal"))]
124 {
125 Some(op.run_metal(seed))
126 }
127 #[cfg(not(all(target_os = "macos", feature = "metal")))]
128 {
129 None
130 }
131 }
132 RequiredBackend::Cuda => {
133 #[cfg(feature = "cuda")]
134 {
135 Some(op.run_cuda(seed))
136 }
137 #[cfg(not(feature = "cuda"))]
138 {
139 None
140 }
141 }
142 }
143}
144
145fn panic_text(payload: Box<dyn Any + Send>) -> String {
146 if let Some(message) = payload.downcast_ref::<String>() {
147 message.clone()
148 } else if let Some(message) = payload.downcast_ref::<&str>() {
149 (*message).into()
150 } else {
151 "non-string panic payload".into()
152 }
153}
154
155pub fn run_required(
159 op: &dyn OpUnderTest,
160 backend: RequiredBackend,
161 seed: u64,
162 tolerance: f64,
163) -> RequiredReport {
164 run_with(
165 op.name(),
166 backend,
167 seed,
168 tolerance,
169 || availability(backend),
170 || op.run_cpu(seed),
171 || backend_output(op, backend, seed),
172 )
173}
174
175fn run_with(
176 op: &str,
177 backend: RequiredBackend,
178 seed: u64,
179 tolerance: f64,
180 probe: impl FnOnce() -> Result<Availability, String>,
181 reference: impl FnOnce() -> Output,
182 execute: impl FnOnce() -> Option<Output>,
183) -> RequiredReport {
184 let mut report = RequiredReport {
185 schema_version: 1,
186 op: op.into(),
187 backend,
188 seed,
189 tolerance: tolerance.is_finite().then_some(tolerance),
190 tolerance_f64_bits: tolerance.to_bits(),
191 status: RequiredStatus::NotRun,
192 reason: None,
193 reference: None,
194 actual: None,
195 metrics: None,
196 };
197 if let Err(error) = valid_tolerance(tolerance) {
198 report.failed(error.reason);
199 return report;
200 }
201 match catch_unwind(AssertUnwindSafe(probe)) {
202 Ok(Ok(Availability::Available)) => {}
203 Ok(Ok(Availability::Unavailable(reason))) => {
204 report.reason = Some(reason);
205 return report;
206 }
207 Ok(Err(error)) => {
208 report.failed(format!("backend availability query failed: {error}"));
209 return report;
210 }
211 Err(payload) => {
212 report.failed(format!(
213 "backend availability query panicked: {}",
214 panic_text(payload)
215 ));
216 return report;
217 }
218 }
219 let reference = match catch_unwind(AssertUnwindSafe(reference)) {
220 Ok(output) => output,
221 Err(payload) => {
222 report.failed(format!("CPU reference panicked: {}", panic_text(payload)));
223 return report;
224 }
225 };
226 report.reference = Some(RawOutput::from_f32(&reference));
227 if let Err(error) = valid_reference(&reference) {
228 report.failed(error.reason);
229 return report;
230 }
231 let actual = match catch_unwind(AssertUnwindSafe(execute)) {
232 Ok(Some(output)) => output,
233 Ok(None) => {
234 report.reason =
235 Some("required backend was not executed; no output was produced".into());
236 return report;
237 }
238 Err(payload) => {
239 report.failed(format!(
240 "required backend execution panicked: {}",
241 panic_text(payload)
242 ));
243 return report;
244 }
245 };
246 report.actual = Some(RawOutput::from_f32(&actual));
247 match compare_outputs(&reference, &actual, tolerance) {
248 Ok(metrics) => {
249 report.metrics = Some(metrics);
250 report.status = RequiredStatus::Passed;
251 }
252 Err(error) => {
253 report.metrics = error.metrics;
254 report.failed(error.reason);
255 }
256 }
257 report
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use std::cell::Cell;
264
265 #[test]
266 fn missing_backend_does_not_even_run_the_reference() {
267 let report = run_with(
268 "fixture",
269 RequiredBackend::Metal,
270 7,
271 1e-7,
272 || Ok(Availability::Unavailable("no device".into())),
273 || panic!("reference must not run"),
274 || panic!("backend must not run"),
275 );
276 assert_eq!(report.status, RequiredStatus::NotRun);
277 assert!(report.reference.is_none());
278 assert!(report.actual.is_none());
279 assert!(!report.is_passed());
280 }
281
282 #[test]
283 fn cpu_reference_alone_cannot_pass_required_backend() {
284 let report = run_with(
285 "fixture",
286 RequiredBackend::Cuda,
287 7,
288 1e-7,
289 || Ok(Availability::Available),
290 || vec![1.0, 2.0],
291 || None,
292 );
293 assert_eq!(report.status, RequiredStatus::NotRun);
294 assert_eq!(report.reference.unwrap().to_f32(), [1.0, 2.0]);
295 assert!(report.actual.is_none());
296 assert!(report.metrics.is_none());
297 }
298
299 #[test]
300 fn failed_probe_and_panicking_execution_are_failed_not_not_run() {
301 let probe = run_with(
302 "fixture",
303 RequiredBackend::Cuda,
304 7,
305 1e-7,
306 || Err("driver query failed".into()),
307 || panic!("no reference"),
308 || panic!("no backend"),
309 );
310 assert_eq!(probe.status, RequiredStatus::Failed);
311 let dispatch = run_with(
312 "fixture",
313 RequiredBackend::Metal,
314 7,
315 1e-7,
316 || Ok(Availability::Available),
317 || vec![1.0],
318 || panic!("checked GPU completion failed"),
319 );
320 assert_eq!(dispatch.status, RequiredStatus::Failed);
321 assert!(dispatch.reference.is_some());
322 assert!(dispatch.actual.is_none());
323 assert!(dispatch
324 .reason
325 .unwrap()
326 .contains("checked GPU completion failed"));
327 }
328
329 #[test]
330 fn runner_and_replay_share_the_same_comparator() {
331 for actual in [vec![1.0, 2.0], vec![2.0, 4.0], vec![], vec![f32::NAN, 2.0]] {
332 let result = run_with(
333 "fixture",
334 RequiredBackend::Metal,
335 7,
336 1e-7,
337 || Ok(Availability::Available),
338 || vec![1.0, 2.0],
339 || Some(actual),
340 );
341 let reference = result.reference.as_ref().unwrap().to_f32();
342 let actual = result.actual.as_ref().unwrap().to_f32();
343 let replay = compare_outputs(
344 &reference,
345 &actual,
346 f64::from_bits(result.tolerance_f64_bits),
347 );
348 assert_eq!(result.is_passed(), replay.is_ok());
349 assert_eq!(
350 result.metrics,
351 match replay {
352 Ok(metrics) => Some(metrics),
353 Err(error) => error.metrics,
354 }
355 );
356 }
357 }
358
359 #[test]
360 fn invalid_reference_or_tolerance_prevents_backend_execution() {
361 let dispatched = Cell::new(false);
362 let result = run_with(
363 "fixture",
364 RequiredBackend::Metal,
365 7,
366 1e-7,
367 || Ok(Availability::Available),
368 || vec![],
369 || {
370 dispatched.set(true);
371 Some(vec![])
372 },
373 );
374 assert_eq!(result.status, RequiredStatus::Failed);
375 assert!(!dispatched.get());
376 let result = run_with(
377 "fixture",
378 RequiredBackend::Metal,
379 7,
380 f64::NAN,
381 || panic!("invalid input must not probe"),
382 || panic!("no reference"),
383 || panic!("no backend"),
384 );
385 assert_eq!(result.status, RequiredStatus::Failed);
386 assert_eq!(result.tolerance, None);
387 assert_eq!(result.tolerance_f64_bits, f64::NAN.to_bits());
388 }
389
390 struct MustNotExecute;
391 impl OpUnderTest for MustNotExecute {
392 fn name(&self) -> &str {
393 "fixture"
394 }
395 fn run_cpu(&self, _: u64) -> Output {
396 panic!("CPU reference must not execute for an uncompiled backend")
397 }
398 #[cfg(all(target_os = "macos", feature = "metal"))]
399 fn run_metal(&self, _: u64) -> Output {
400 panic!("unrequested Metal must not execute")
401 }
402 #[cfg(feature = "cuda")]
403 fn run_cuda(&self, _: u64) -> Output {
404 panic!("unrequested CUDA must not execute")
405 }
406 }
407
408 #[cfg(not(all(target_os = "macos", feature = "metal")))]
409 #[test]
410 fn missing_metal_feature_is_not_run_through_public_api() {
411 let result = run_required(&MustNotExecute, RequiredBackend::Metal, 7, 1e-7);
412 assert_eq!(result.status, RequiredStatus::NotRun);
413 assert!(result.actual.is_none());
414 }
415
416 #[cfg(not(feature = "cuda"))]
417 #[test]
418 fn missing_cuda_feature_is_not_run_through_public_api() {
419 let result = run_required(&MustNotExecute, RequiredBackend::Cuda, 7, 1e-7);
420 assert_eq!(result.status, RequiredStatus::NotRun);
421 assert!(result.actual.is_none());
422 }
423}