1#![cfg(not(target_arch = "wasm32"))]
17
18use serde::{Deserialize, Serialize};
19use std::io::{Read, Write};
20use std::process::Command;
21use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
22use std::time::Instant;
23
24const GEMV_BENCH_WGSL: &str = include_str!("../shaders/gemv_bench.wgsl");
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28pub enum CircuitKind {
29 DiscreteGpu,
30 IntegratedGpu,
31 Cpu,
32 Npu,
33 Other,
34}
35
36impl CircuitKind {
37 fn from_wgpu(t: wgpu::DeviceType) -> Self {
38 match t {
39 wgpu::DeviceType::DiscreteGpu => Self::DiscreteGpu,
40 wgpu::DeviceType::IntegratedGpu => Self::IntegratedGpu,
41 wgpu::DeviceType::Cpu => Self::Cpu,
42 _ => Self::Other,
43 }
44 }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct CircuitBench {
50 pub label: String,
51 pub kind: CircuitKind,
52 pub backend: String,
53 pub ms_per_gemv: f64,
55 pub gflops: f64,
57 pub upload_gbps: f64,
62 pub rel_score: f64,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub decode_proxy_tok_s: Option<f64>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct DeviceBenchmarkRequest {
74 pub backend: String,
75 pub vendor: u32,
76 pub device: u32,
77 pub gemv_n: usize,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct DeviceBenchmarkResponse {
82 pub request: DeviceBenchmarkRequest,
83 pub bench: Option<CircuitBench>,
84 pub error: Option<String>,
85}
86
87#[cfg(not(test))]
88const WORKER_ENV: &str = "QUALIA_DEVICE_BENCHMARK_WORKER";
89const WORKER_OUTPUT_ENV: &str = "QUALIA_DEVICE_BENCHMARK_OUTPUT";
90static WORKER_SEQUENCE: AtomicU64 = AtomicU64::new(0);
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct CapabilityMatrix {
95 pub circuits: Vec<CircuitBench>,
96 pub gemv_n: usize,
97 pub npu_probed: bool,
99}
100
101impl CapabilityMatrix {
102 pub fn best(&self) -> Option<&CircuitBench> {
104 self.circuits.first()
105 }
106
107 pub fn summary(&self) -> String {
108 let mut s = format!(
109 "CapabilityMatrix (GEMV {n}x{n}, ranked by measured throughput; NPU probed={npu}):\n",
110 n = self.gemv_n,
111 npu = self.npu_probed
112 );
113 for (i, c) in self.circuits.iter().enumerate() {
114 let upload = if c.upload_gbps.is_infinite() {
115 "in-pool".to_string()
116 } else {
117 format!("{:.1} GB/s up", c.upload_gbps)
118 };
119 let decode = match c.decode_proxy_tok_s {
120 Some(t) => format!("{t:.2} tok/s"),
121 None => "—".into(),
122 };
123 s.push_str(&format!(
124 " {}. {:<28} [{:?}/{}] {:>8.3} ms {:>7.1} GFLOP/s {:>12} decode {:>10} score {:.3}\n",
125 i + 1,
126 c.label,
127 c.kind,
128 c.backend,
129 c.ms_per_gemv,
130 c.gflops,
131 upload,
132 decode,
133 c.rel_score,
134 ));
135 }
136 s
137 }
138
139 pub fn apply_decode_proxy_ranking(&mut self) {
142 let any_decode = self
143 .circuits
144 .iter()
145 .any(|c| c.decode_proxy_tok_s.is_some() && c.kind != CircuitKind::Cpu);
146 if !any_decode {
147 return;
148 }
149 self.circuits.sort_by(|a, b| {
150 let ta = a.decode_proxy_tok_s.unwrap_or(-1.0);
151 let tb = b.decode_proxy_tok_s.unwrap_or(-1.0);
152 match tb.partial_cmp(&ta).unwrap_or(std::cmp::Ordering::Equal) {
154 std::cmp::Ordering::Equal => a
155 .ms_per_gemv
156 .partial_cmp(&b.ms_per_gemv)
157 .unwrap_or(std::cmp::Ordering::Equal),
158 o => o,
159 }
160 });
161 if let Some(best_t) = self
162 .circuits
163 .iter()
164 .filter_map(|c| c.decode_proxy_tok_s)
165 .fold(None, |acc: Option<f64>, t| {
166 Some(acc.map(|a| a.max(t)).unwrap_or(t))
167 })
168 {
169 for c in &mut self.circuits {
170 c.rel_score = match c.decode_proxy_tok_s {
171 Some(t) if best_t > 0.0 => t / best_t,
172 _ => 0.0,
173 };
174 }
175 }
176 }
177}
178
179#[inline]
180fn params_bytes(n_in: u32, n_out: u32) -> [u8; 16] {
181 let mut b = [0u8; 16];
182 b[0..4].copy_from_slice(&n_in.to_le_bytes());
183 b[4..8].copy_from_slice(&n_out.to_le_bytes());
184 b
185}
186
187fn bench_gpu_gemv(device: &wgpu::Device, queue: &wgpu::Queue, n: usize) -> f64 {
190 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
191 label: Some("gemv_bench"),
192 source: wgpu::ShaderSource::Wgsl(GEMV_BENCH_WGSL.into()),
193 });
194 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
195 label: Some("gemv_bench_pipeline"),
196 layout: None,
197 module: &shader,
198 entry_point: Some("gemv"),
199 compilation_options: Default::default(),
200 cache: None,
201 });
202 let mk = |contents: &[u8], usage: wgpu::BufferUsages| {
203 let b = device.create_buffer(&wgpu::BufferDescriptor {
204 label: None,
205 size: contents.len().max(4) as u64,
206 usage,
207 mapped_at_creation: false,
208 });
209 if !contents.is_empty() {
210 queue.write_buffer(&b, 0, contents);
211 }
212 b
213 };
214 let input = mk(
215 bytemuck::cast_slice(&vec![0.1f32; n]),
216 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
217 );
218 let weight = mk(
219 bytemuck::cast_slice(&vec![0.05f32; n * n]),
220 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
221 );
222 let params = mk(
223 ¶ms_bytes(n as u32, n as u32),
224 wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
225 );
226 let out = device.create_buffer(&wgpu::BufferDescriptor {
227 label: None,
228 size: (n * 4).max(4) as u64,
229 usage: wgpu::BufferUsages::STORAGE,
230 mapped_at_creation: false,
231 });
232 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
233 label: None,
234 layout: &pipeline.get_bind_group_layout(0),
235 entries: &[
236 wgpu::BindGroupEntry {
237 binding: 0,
238 resource: input.as_entire_binding(),
239 },
240 wgpu::BindGroupEntry {
241 binding: 1,
242 resource: weight.as_entire_binding(),
243 },
244 wgpu::BindGroupEntry {
245 binding: 2,
246 resource: params.as_entire_binding(),
247 },
248 wgpu::BindGroupEntry {
249 binding: 3,
250 resource: out.as_entire_binding(),
251 },
252 ],
253 });
254 let wg_x = (n as u32).div_ceil(64).max(1);
255 let (k, s) = (16u32, 5u32);
256 let submit_batch = || {
257 let mut enc =
258 device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
259 {
260 let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
261 label: None,
262 timestamp_writes: None,
263 });
264 pass.set_pipeline(&pipeline);
265 pass.set_bind_group(0, &bind, &[]);
266 for _ in 0..k {
267 pass.dispatch_workgroups(wg_x, 1, 1);
268 }
269 }
270 queue.submit(Some(enc.finish()));
271 let _ = device.poll(wgpu::PollType::wait_indefinitely());
272 };
273 submit_batch(); let t0 = Instant::now();
275 for _ in 0..s {
276 submit_batch();
277 }
278 t0.elapsed().as_secs_f64() * 1e3 / (k * s) as f64
279}
280
281fn bench_upload_gbps(device: &wgpu::Device, queue: &wgpu::Queue, bytes: usize) -> f64 {
286 let data = vec![0u8; bytes];
287 let buf = device.create_buffer(&wgpu::BufferDescriptor {
288 label: Some("upload_probe"),
289 size: bytes as u64,
290 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
291 mapped_at_creation: false,
292 });
293 let flush = || {
294 let enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
295 queue.submit(Some(enc.finish()));
296 let _ = device.poll(wgpu::PollType::wait_indefinitely());
297 };
298 queue.write_buffer(&buf, 0, &data); flush();
300 let iters = 3u32;
301 let t0 = Instant::now();
302 for _ in 0..iters {
303 queue.write_buffer(&buf, 0, &data);
304 flush();
305 }
306 let secs = t0.elapsed().as_secs_f64() / iters as f64;
307 if secs <= 0.0 {
308 0.0
309 } else {
310 bytes as f64 / secs / 1e9
311 }
312}
313
314fn bench_cpu_gemv(n: usize) -> f64 {
316 use rayon::prelude::*;
317 let weight = vec![0.05f32; n * n];
318 let input = vec![0.1f32; n];
319 let mut out = vec![0.0f32; n];
320 let gemv = |out: &mut [f32]| {
321 out.par_iter_mut().enumerate().for_each(|(i, o)| {
322 let row = &weight[i * n..(i + 1) * n];
323 *o = row.iter().zip(input.iter()).map(|(a, b)| a * b).sum();
324 });
325 };
326 gemv(&mut out); let iters = 5u32;
328 let t0 = Instant::now();
329 for _ in 0..iters {
330 gemv(&mut out);
331 }
332 t0.elapsed().as_secs_f64() * 1e3 / iters as f64
333}
334
335#[inline]
336fn gflops(n: usize, ms: f64) -> f64 {
337 if ms <= 0.0 {
338 0.0
339 } else {
340 (2.0 * n as f64 * n as f64) / (ms / 1e3) / 1e9
341 }
342}
343
344fn backend_rank(b: wgpu::Backend) -> u8 {
345 match b {
346 wgpu::Backend::Metal => 0,
347 wgpu::Backend::Dx12 => 1,
348 wgpu::Backend::Vulkan => 2,
349 wgpu::Backend::Gl => 3,
350 _ => 4,
351 }
352}
353
354fn backend_name(backend: wgpu::Backend) -> &'static str {
355 match backend {
356 wgpu::Backend::Vulkan => "vulkan",
357 wgpu::Backend::Dx12 => "dx12",
358 wgpu::Backend::Metal => "metal",
359 wgpu::Backend::Gl => "gl",
360 wgpu::Backend::BrowserWebGpu => "browser-webgpu",
361 wgpu::Backend::Noop => "noop",
362 }
363}
364
365fn backend_from_name(name: &str) -> Option<(wgpu::Backend, wgpu::Backends)> {
366 match name {
367 "vulkan" => Some((wgpu::Backend::Vulkan, wgpu::Backends::VULKAN)),
368 "dx12" => Some((wgpu::Backend::Dx12, wgpu::Backends::DX12)),
369 "metal" => Some((wgpu::Backend::Metal, wgpu::Backends::METAL)),
370 "gl" => Some((wgpu::Backend::Gl, wgpu::Backends::GL)),
371 _ => None,
372 }
373}
374
375fn encode_response(
376 path: &std::path::Path,
377 response: &DeviceBenchmarkResponse,
378) -> Result<(), String> {
379 let mut payload = Vec::new();
380 ciborium::into_writer(response, &mut payload).map_err(|e| format!("encode response: {e}"))?;
381 let mut file =
382 std::fs::File::create(path).map_err(|e| format!("create {}: {e}", path.display()))?;
383 file.write_all(&(payload.len() as u64).to_le_bytes())
384 .map_err(|e| e.to_string())?;
385 file.write_all(&payload).map_err(|e| e.to_string())?;
386 file.sync_all().map_err(|e| e.to_string())
387}
388
389fn decode_response(path: &std::path::Path) -> Result<DeviceBenchmarkResponse, String> {
390 let mut file =
391 std::fs::File::open(path).map_err(|e| format!("open {}: {e}", path.display()))?;
392 let mut length = [0u8; 8];
393 file.read_exact(&mut length)
394 .map_err(|e| format!("read response length: {e}"))?;
395 let length = u64::from_le_bytes(length) as usize;
396 if length == 0 || length > 1024 * 1024 {
397 return Err(format!("invalid worker response length {length}"));
398 }
399 let mut payload = vec![0u8; length];
400 file.read_exact(&mut payload)
401 .map_err(|e| format!("read response: {e}"))?;
402 ciborium::from_reader(payload.as_slice()).map_err(|e| format!("decode response: {e}"))
403}
404
405fn benchmark_one(request: &DeviceBenchmarkRequest) -> Result<CircuitBench, String> {
406 let (expected_backend, backends) = backend_from_name(&request.backend)
407 .ok_or_else(|| format!("unsupported backend {}", request.backend))?;
408 let mut descriptor = wgpu::InstanceDescriptor::new_without_display_handle();
409 descriptor.backends = backends;
410 let instance = wgpu::Instance::new(descriptor);
411 let adapters = pollster::block_on(instance.enumerate_adapters(backends));
412 let adapter = adapters
413 .into_iter()
414 .find(|adapter| {
415 let info = adapter.get_info();
416 info.backend == expected_backend
417 && info.vendor == request.vendor
418 && info.device == request.device
419 })
420 .ok_or_else(|| {
421 format!(
422 "adapter {:04x}:{:04x}/{} unavailable",
423 request.vendor, request.device, request.backend
424 )
425 })?;
426 let info = adapter.get_info();
427 let (device, queue) =
428 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default()))
429 .map_err(|e| format!("request_device: {e}"))?;
430 let ms = bench_gpu_gemv(&device, &queue, request.gemv_n);
431 let upload_gbps = bench_upload_gbps(&device, &queue, 16 * 1024 * 1024);
432 Ok(CircuitBench {
433 label: format!("{} ({:?})", info.name, info.backend),
434 kind: CircuitKind::from_wgpu(info.device_type),
435 backend: format!("{:?}", info.backend),
436 ms_per_gemv: ms,
437 gflops: gflops(request.gemv_n, ms),
438 upload_gbps,
439 rel_score: 1.0,
440 decode_proxy_tok_s: None,
441 })
442}
443
444pub fn run_worker_from_env() -> Result<(), String> {
446 let request: DeviceBenchmarkRequest = DeviceBenchmarkRequest {
447 backend: std::env::var("QUALIA_DEVICE_BENCHMARK_BACKEND").map_err(|_| "missing backend")?,
448 vendor: std::env::var("QUALIA_DEVICE_BENCHMARK_VENDOR")
449 .map_err(|_| "missing vendor")?
450 .parse()
451 .map_err(|_| "invalid vendor")?,
452 device: std::env::var("QUALIA_DEVICE_BENCHMARK_DEVICE")
453 .map_err(|_| "missing device")?
454 .parse()
455 .map_err(|_| "invalid device")?,
456 gemv_n: std::env::var("QUALIA_DEVICE_BENCHMARK_N")
457 .map_err(|_| "missing n")?
458 .parse()
459 .map_err(|_| "invalid n")?,
460 };
461 let response = match benchmark_one(&request) {
462 Ok(bench) => DeviceBenchmarkResponse {
463 request,
464 bench: Some(bench),
465 error: None,
466 },
467 Err(error) => DeviceBenchmarkResponse {
468 request,
469 bench: None,
470 error: Some(error),
471 },
472 };
473 let output = std::env::var_os(WORKER_OUTPUT_ENV).ok_or("missing worker output path")?;
474 encode_response(std::path::Path::new(&output), &response)
475}
476
477#[cfg(not(test))]
478fn worker_executable() -> Option<std::path::PathBuf> {
479 if let Some(path) = std::env::var_os(WORKER_ENV) {
480 return Some(path.into());
481 }
482 let current = std::env::current_exe().ok()?;
483 let sibling = current.with_file_name(if cfg!(windows) {
484 "qualia-device-benchmark-worker.exe"
485 } else {
486 "qualia-device-benchmark-worker"
487 });
488 if sibling.is_file() {
489 Some(sibling)
490 } else if current
491 .file_stem()
492 .and_then(|s| s.to_str())
493 .is_some_and(|name| name == "qualia-cli" || name == "webizen-desktop")
494 {
495 Some(current)
499 } else {
500 None
501 }
502}
503
504fn invoke_worker(request: &DeviceBenchmarkRequest) -> Result<CircuitBench, String> {
505 let sequence = WORKER_SEQUENCE.fetch_add(1, AtomicOrdering::Relaxed);
506 let output = std::env::temp_dir().join(format!(
507 "qualia-device-bench-{}-{sequence}.cbor",
508 std::process::id()
509 ));
510 let mut command;
511 #[cfg(test)]
512 {
513 command = Command::new(std::env::current_exe().map_err(|e| e.to_string())?);
514 command.args([
515 "--exact",
516 "platform::device_benchmark::tests::device_benchmark_worker_entry",
517 "--nocapture",
518 ]);
519 }
520 #[cfg(not(test))]
521 {
522 command = Command::new(
523 worker_executable().ok_or_else(|| format!("worker not found; set {WORKER_ENV}"))?,
524 );
525 }
526 let mut child = command
527 .env("QUALIA_DEVICE_BENCHMARK_BACKEND", &request.backend)
528 .env("QUALIA_DEVICE_BENCHMARK_VENDOR", request.vendor.to_string())
529 .env("QUALIA_DEVICE_BENCHMARK_DEVICE", request.device.to_string())
530 .env("QUALIA_DEVICE_BENCHMARK_N", request.gemv_n.to_string())
531 .env(WORKER_OUTPUT_ENV, &output)
532 .spawn()
533 .map_err(|e| format!("launch worker: {e}"))?;
534 let deadline = Instant::now() + std::time::Duration::from_secs(120);
535 let status = loop {
536 if let Some(status) = child
537 .try_wait()
538 .map_err(|e| format!("wait for worker: {e}"))?
539 {
540 break status;
541 }
542 if Instant::now() >= deadline {
543 let _ = child.kill();
544 let _ = child.wait();
545 let _ = std::fs::remove_file(&output);
546 return Err("worker exceeded 120-second deadline".into());
547 }
548 std::thread::sleep(std::time::Duration::from_millis(25));
549 };
550 let decoded = if status.success() {
551 decode_response(&output)
552 } else {
553 Err(format!("worker exited {status}"))
554 };
555 let _ = std::fs::remove_file(&output);
556 let response = decoded?;
557 if response.request.backend != request.backend
558 || response.request.vendor != request.vendor
559 || response.request.device != request.device
560 || response.request.gemv_n != request.gemv_n
561 {
562 return Err("worker response identity mismatch".into());
563 }
564 if let Some(error) = response.error {
565 return Err(error);
566 }
567 let bench = response
568 .bench
569 .ok_or("worker returned neither result nor error")?;
570 if !bench.ms_per_gemv.is_finite()
571 || bench.ms_per_gemv <= 0.0
572 || !bench.gflops.is_finite()
573 || !bench.upload_gbps.is_finite()
574 || bench.upload_gbps < 0.0
575 {
576 return Err("worker returned invalid metrics".into());
577 }
578 Ok(bench)
579}
580
581pub fn benchmark_devices(n: usize) -> CapabilityMatrix {
588 let mut circuits: Vec<CircuitBench> = Vec::new();
589
590 let instance = wgpu::Instance::default();
592 let mut cand: Vec<(u8, wgpu::Adapter, wgpu::AdapterInfo)> = Vec::new();
593 for adapter in pollster::block_on(instance.enumerate_adapters(wgpu::Backends::all())) {
594 let info = adapter.get_info();
595 if info.device_type == wgpu::DeviceType::Cpu || info.device == 0 {
596 continue;
597 }
598 cand.push((backend_rank(info.backend), adapter, info));
599 }
600 cand.sort_by_key(|(r, _, _)| *r);
601 let mut seen: std::collections::HashSet<(u32, u32, u32)> = std::collections::HashSet::new();
603 let mut chosen: Vec<DeviceBenchmarkRequest> = Vec::new();
604 for (_, _adapter, info) in cand {
605 let backend_id = info.backend as u32;
606 if seen.insert((info.vendor, info.device, backend_id)) {
607 chosen.push(DeviceBenchmarkRequest {
608 backend: backend_name(info.backend).to_string(),
609 vendor: info.vendor,
610 device: info.device,
611 gemv_n: n,
612 });
613 }
614 }
615 for request in chosen {
616 match invoke_worker(&request) {
617 Ok(bench) => circuits.push(bench),
618 Err(error) => log::warn!(
619 "device_benchmark|skip|{:04x}:{:04x}|{}|{}",
620 request.vendor,
621 request.device,
622 request.backend,
623 error
624 ),
625 }
626 }
630
631 let cpu_ms = bench_cpu_gemv(n);
633 circuits.push(CircuitBench {
634 label: format!("CPU native (rayon, {} cores)", num_cpus::get()),
635 kind: CircuitKind::Cpu,
636 backend: "native".to_string(),
637 ms_per_gemv: cpu_ms,
638 gflops: gflops(n, cpu_ms),
639 upload_gbps: f64::INFINITY, rel_score: 1.0,
641 decode_proxy_tok_s: None,
642 });
643
644 circuits.sort_by(|a, b| {
646 a.ms_per_gemv
647 .partial_cmp(&b.ms_per_gemv)
648 .unwrap_or(std::cmp::Ordering::Equal)
649 .then_with(|| a.backend.cmp(&b.backend))
650 .then_with(|| a.label.cmp(&b.label))
651 });
652 if let Some(best_ms) = circuits.first().map(|c| c.ms_per_gemv) {
653 for c in &mut circuits {
654 c.rel_score = if c.ms_per_gemv > 0.0 {
655 best_ms / c.ms_per_gemv
656 } else {
657 0.0
658 };
659 }
660 }
661
662 CapabilityMatrix {
663 circuits,
664 gemv_n: n,
665 npu_probed: false,
666 }
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 #[test]
674 fn device_benchmark_worker_entry() {
675 if std::env::var_os(WORKER_OUTPUT_ENV).is_some() {
676 run_worker_from_env().expect("device benchmark worker must write its response");
677 }
678 }
679
680 #[test]
681 fn worker_protocol_round_trips() {
682 let request = DeviceBenchmarkRequest {
683 backend: "vulkan".into(),
684 vendor: 1,
685 device: 2,
686 gemv_n: 32,
687 };
688 let response = DeviceBenchmarkResponse {
689 request: request.clone(),
690 bench: None,
691 error: Some("expected".into()),
692 };
693 let path = std::env::temp_dir().join(format!(
694 "qualia-device-protocol-{}.cbor",
695 std::process::id()
696 ));
697 encode_response(&path, &response).unwrap();
698 let decoded = decode_response(&path).unwrap();
699 let _ = std::fs::remove_file(path);
700 assert_eq!(decoded.request.backend, request.backend);
701 assert_eq!(decoded.error.as_deref(), Some("expected"));
702 }
703
704 #[test]
707 #[serial_test::serial(gpu)]
708 fn h1a_capability_matrix() {
709 let matrix = benchmark_devices(2048);
710 eprintln!("{}", matrix.summary());
711
712 assert!(
713 !matrix.circuits.is_empty(),
714 "at least the CPU circuit must be benchmarked"
715 );
716 assert!(
717 matrix.circuits.iter().any(|c| c.kind == CircuitKind::Cpu),
718 "native CPU circuit must always be present"
719 );
720 for w in matrix.circuits.windows(2) {
722 assert!(
723 w[0].ms_per_gemv <= w[1].ms_per_gemv + 1e-9,
724 "matrix must be sorted fastest-first"
725 );
726 }
727 assert!(
728 (matrix.best().unwrap().rel_score - 1.0).abs() < 1e-9,
729 "best score must be 1.0"
730 );
731 }
732}