Skip to main content

gam_gpu/
encode_throughput.rs

1//! Measured device-resident throughput of the SAE/LLM batched-solve COMPONENT —
2//! the resident penalized normal-equations inner solve, NOT the full exact SAE
3//! encode (see the SCOPE section below) (#1412, #988, #1017 Phase-3).
4//!
5//! ## Why this module exists
6//!
7//! The historical throughput "decision gate" (#1412) asserted a `100_000`
8//! rows/sec/GPU deployment target **without ever measuring a device**. Its
9//! successor still keyed the deployment decision on a *CPU* measurement scaled
10//! by a hardcoded `CPU_TO_GPU_SCALING = 100.0` fudge factor — so passing the
11//! gate established nothing about real GPU throughput. #988 closed
12//! `COMPLETED` while the maintainer's own follow-up confirmed the GPU
13//! steady-state encode rate had never been measured.
14//!
15//! This module makes the measurement real and *testable as a library function*
16//! (the prior real benchmark lived only in `examples/throughput_1412.rs`, which
17//! nothing in CI ran or asserted). [`measure_resident_solve_throughput`] runs
18//! the production IRLS inner step — upload `X` once, then repeatedly solve the
19//! penalized normal equations `(XᵀWX + ridge·I)β = rhs` with the `p×p` Gram and
20//! its Cholesky factor kept DEVICE-RESIDENT, downloading only the `p`-vector
21//! `β` — on the real device, and reports the measured design-rows/sec.
22//!
23//! ## SCOPE — this is a COMPONENT benchmark, not the full exact SAE encode
24//!
25//! What is timed here is the resident penalized normal-equations *inner solve*
26//! `(XᵀWX + ridge·I)β = rhs` ONLY. That is one component of the SAE encode, NOT
27//! the full exact per-row SAE encode, and the measured rate is therefore NOT
28//! evidence for a "batched exact per-row GPU encode" title claim. The full exact
29//! encode would additionally require, per row: active-set routing (which atoms
30//! are live), the per-row latent-coordinate Newton refinement on the manifold,
31//! the assignment/gate (softmax/IBP) solve, and the certificate/fallback +
32//! reconstruction-validation path. None of those are exercised or timed by this
33//! function. Establishing the end-to-end encode-throughput claim requires a
34//! separate benchmark that times the *production encode path itself* (routing +
35//! latent-coordinate Newton + assignment/gate solve + fallback/certificate), not
36//! this inner-solve cell. Treat the number below strictly as the resident
37//! normal-equations inner-solve throughput.
38//!
39//! ## Fail-loud, never false-route
40//!
41//! The single recurring failure mode this guards against is *false GPU
42//! routing*: claiming a device measurement while the work silently ran on the
43//! CPU. [`ResidentSolveThroughput::engaged`] is `true` only when
44//! [`ResidentDesignGram::try_new`] actually staged `X` on the device AND every
45//! timed solve returned a device result. If the device path declines or fails
46//! mid-measurement, `engaged` is `false` and `measured_rows_per_sec` is left at
47//! `0.0` — a non-measurement that [`GpuThroughputVerdict`] can never report as
48//! meeting the target. There is no CPU fallback inside the measurement: a
49//! caller that wants the CPU oracle runs it separately for parity.
50
51use std::hint::black_box;
52use std::time::{Duration, Instant};
53
54use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
55
56use super::linalg_dispatch::ResidentDesignGram;
57use super::policy::{GPU_THROUGHPUT_TARGET_ROWS_PER_SEC, GpuThroughputVerdict};
58
59/// A representative LLM/SAE batched-solve work cell: `n` design rows, `p` wide
60/// decoder border. (`d`, the per-atom reduced-Schur block size, is fixed by the
61/// term and does not enter the resident-solve throughput.)
62#[derive(Clone, Copy, Debug)]
63pub struct EncodeShape {
64    /// Human-readable label for reporting.
65    pub label: &'static str,
66    /// Design rows pushed through the device per fit.
67    pub n: usize,
68    /// Decoder-border width (the resident Gram is `p×p`).
69    pub p: usize,
70}
71
72/// The canonical qwen/olmo-scale SAE residual-block shapes (matches the
73/// `examples/throughput_1412.rs` workload so the library measurement and the
74/// example agree).
75pub const CANONICAL_ENCODE_SHAPES: &[EncodeShape] = &[
76    EncodeShape {
77        label: "sae-2k-2048",
78        n: 2_000,
79        p: 2_048,
80    },
81    EncodeShape {
82        label: "sae-4k-4096",
83        n: 4_000,
84        p: 4_096,
85    },
86    EncodeShape {
87        label: "sae-8k-1024",
88        n: 8_000,
89        p: 1_024,
90    },
91];
92
93/// Outcome of measuring the device-resident penalized-solve throughput for one
94/// [`EncodeShape`].
95#[derive(Clone, Copy, Debug)]
96pub struct ResidentSolveThroughput {
97    /// The shape that was measured.
98    pub shape: EncodeShape,
99    /// `true` iff `X` was staged on the device AND every timed solve returned a
100    /// device result. `false` means the device path declined or failed — the
101    /// number below is **not** a device measurement.
102    pub engaged: bool,
103    /// Measured design-rows/sec for the resident solve, or `0.0` when the
104    /// device path did not engage (a non-measurement).
105    pub measured_rows_per_sec: f64,
106    /// The verdict comparing `measured_rows_per_sec` against
107    /// [`GPU_THROUGHPUT_TARGET_ROWS_PER_SEC`].
108    pub verdict: GpuThroughputVerdict,
109}
110
111/// Deterministic LCG in `[-1, 1)` — no `rand` dependency, fully reproducible
112/// across runs so the measured fixture is stable.
113fn lcg(state: &mut u64) -> f64 {
114    *state = state
115        .wrapping_mul(6364136223846793005)
116        .wrapping_add(1442695040888963407);
117    (*state >> 11) as f64 / (1u64 << 53) as f64 * 2.0 - 1.0
118}
119
120/// Build a deterministic `n×p` design fixture for the throughput measurement.
121fn planted_design(n: usize, p: usize, seed: u64) -> Array2<f64> {
122    let mut s = seed;
123    Array2::from_shape_fn((n, p), |_| lcg(&mut s) * 0.05)
124}
125
126/// Measure the device-resident penalized-normal-equations solve throughput for
127/// one shape: upload `X` once, then time `reps` solves that cross only `w`
128/// (H2D), `rhs` (H2D, fixed), and `β` (D2H) — the production IRLS inner step.
129///
130/// `reps` is the number of timed solves; `w` is perturbed per rep so each solve
131/// is genuine work, mirroring an IRLS weight update. Returns a
132/// [`ResidentSolveThroughput`] whose `engaged` flag is the false-routing guard:
133/// on a CPU-only host (or if the device declines) it is `false` and the rate is
134/// `0.0`.
135#[must_use]
136pub fn measure_resident_solve_throughput(
137    shape: EncodeShape,
138    reps: usize,
139) -> ResidentSolveThroughput {
140    let EncodeShape { n, p, .. } = shape;
141    let not_engaged = |shape| ResidentSolveThroughput {
142        shape,
143        engaged: false,
144        measured_rows_per_sec: 0.0,
145        verdict: GpuThroughputVerdict::from_measurement(0.0),
146    };
147    if n == 0 || p == 0 || reps == 0 {
148        return not_engaged(shape);
149    }
150
151    let x = planted_design(n, p, 0x1412_a100_dead_beef);
152    let w = {
153        let mut s = 0x988_5ae_e0c0_de01u64;
154        Array1::from_shape_fn(n, |_| lcg(&mut s).abs() + 1e-3)
155    };
156    let rhs = Array1::from_shape_fn(p, |j| ((j as f64 + 1.0) * 0.03).cos());
157    let ridge = 1e-3_f64;
158
159    // Stage X once. `None` => no device / shape below the Gram threshold => not
160    // a device measurement.
161    let handle = match ResidentDesignGram::try_new(x.view()) {
162        Some(h) => h,
163        None => return not_engaged(shape),
164    };
165
166    // Warm the resident solve (allocations, kernel handles) outside the timer;
167    // if even the warm solve declines, the device path is not usable here.
168    if handle
169        .solve_normal_equations(w.view(), rhs.view(), ridge)
170        .is_none()
171    {
172        return not_engaged(shape);
173    }
174
175    let mut total = Duration::ZERO;
176    for r in 0..reps {
177        let wr = Array1::from_shape_fn(n, |i| (w[i] + 1e-3 * (r as f64)).abs());
178        let start = Instant::now();
179        match handle.solve_normal_equations(wr.view(), rhs.view(), ridge) {
180            Some(beta) => {
181                black_box(beta);
182            }
183            // A mid-measurement decline means the timed region is no longer a
184            // pure device measurement — refuse to report it as one.
185            None => return not_engaged(shape),
186        }
187        total += start.elapsed();
188    }
189
190    let secs = total.as_secs_f64() / reps as f64;
191    let measured_rows_per_sec = if secs > 0.0 { n as f64 / secs } else { 0.0 };
192    ResidentSolveThroughput {
193        shape,
194        engaged: measured_rows_per_sec > 0.0,
195        measured_rows_per_sec,
196        verdict: GpuThroughputVerdict::from_measurement(measured_rows_per_sec),
197    }
198}
199
200/// CPU oracle for the same penalized normal-equations solve, used for parity:
201/// `(XᵀWX + ridge·I)β = rhs` solved by a host Cholesky. This is the definition
202/// of truth the device solve must match (up to IEEE-754 reduction order).
203#[must_use]
204pub fn cpu_oracle_normal_equations_solve(
205    x: ArrayView2<'_, f64>,
206    w: ArrayView1<'_, f64>,
207    rhs: ArrayView1<'_, f64>,
208    ridge: f64,
209) -> Array1<f64> {
210    let (n, p) = x.dim();
211    assert_eq!(w.len(), n, "w must have one entry per design row");
212    assert_eq!(rhs.len(), p, "rhs must have one entry per border column");
213
214    // Gram = Xᵀ diag(w) X + ridge·I, formed in f64 as (√w⊙X)ᵀ(√w⊙X) via the
215    // BLAS-backed `dot` (the scalar triple loop is O(n·p²) and dominates the
216    // oracle at p in the thousands). Folding √w into both factors keeps the
217    // weighting exact: row i contributes wᵢ·xᵢₐ·xᵢᵦ as (√wᵢxᵢₐ)(√wᵢxᵢᵦ).
218    let mut xw = x.to_owned();
219    for i in 0..n {
220        let sw = w[i].sqrt();
221        for a in 0..p {
222            xw[[i, a]] *= sw;
223        }
224    }
225    let mut gram = xw.t().dot(&xw);
226    for j in 0..p {
227        gram[[j, j]] += ridge;
228    }
229
230    // Cholesky: gram = L Lᵀ (lower), then solve L y = rhs, Lᵀ β = y.
231    let mut l = Array2::<f64>::zeros((p, p));
232    for j in 0..p {
233        let mut diag = gram[[j, j]];
234        for s in 0..j {
235            diag -= l[[j, s]] * l[[j, s]];
236        }
237        // The oracle exists to be the truth the device is checked against, so a
238        // non-PD pivot must fail loudly here rather than clamp to 0 and launder
239        // a divide-by-zero into a silent NaN in the back-substitution. For the
240        // ridge·I + XᵀWX systems this is called on (ridge > 0, w > 0) the pivot
241        // is always strictly positive; a non-positive pivot means the caller
242        // passed a degenerate system and parity would be meaningless.
243        assert!(
244            diag > 0.0,
245            "cpu_oracle: non-positive Cholesky pivot {diag:.3e} at index {j} — \
246             the Gram is not positive-definite (need ridge>0 and w>0)"
247        );
248        let ljj = diag.sqrt();
249        l[[j, j]] = ljj;
250        for i in (j + 1)..p {
251            let mut off = gram[[i, j]];
252            for s in 0..j {
253                off -= l[[i, s]] * l[[j, s]];
254            }
255            l[[i, j]] = off / ljj;
256        }
257    }
258    let mut y = rhs.to_owned();
259    for i in 0..p {
260        let mut acc = y[i];
261        for s in 0..i {
262            acc -= l[[i, s]] * y[s];
263        }
264        y[i] = acc / l[[i, i]];
265    }
266    let mut beta = y;
267    for i in (0..p).rev() {
268        let mut acc = beta[i];
269        for s in (i + 1)..p {
270            acc -= l[[s, i]] * beta[s];
271        }
272        beta[i] = acc / l[[i, i]];
273    }
274    beta
275}
276
277/// The deployment target, re-exported so callers measuring throughput do not
278/// have to import the policy module directly.
279pub const DEPLOYMENT_TARGET_ROWS_PER_SEC: f64 = GPU_THROUGHPUT_TARGET_ROWS_PER_SEC;
280
281// ===========================================================================
282// FULL exact per-row encode throughput + correctness (#1412 follow-up).
283//
284// The component benchmark above times ONLY the resident normal-equations inner
285// solve `(XᵀWX+ridge·I)β=rhs` and is explicit (see the SCOPE section) that this
286// is NOT the full exact per-row SAE encode. The pieces below are the reusable,
287// gam-sae-free instrument for benchmarking the *full* production encode path
288// end-to-end — active-set/chart routing + per-row latent-coordinate Newton +
289// gate/assignment (amplitude) + Kantorovich certificate/fallback +
290// reconstruction. They live here (CPU-linkable, no `gam-sae` dependency: this
291// crate is *below* `gam-sae`) so the timing harness and the correctness gate
292// are shared, while the driver that actually calls the production
293// `EncodeAtlas::certified_encode_batch` lives in
294// `crates/gam-gpu/tests/encode_full_path_throughput.rs` (a dev-dependency cycle
295// onto `gam-sae`, allowed by cargo for test-only edges).
296//
297// HONEST DEVICE STATUS. This helper is still backend-agnostic instrumentation:
298// callers must set `device_encode_engaged` to `true` only when their encode was
299// produced by a real device-resident exact-encode kernel. The current SAE device
300// driver that can make that assertion lives in
301// `gam_sae::gpu_kernels::sae_encode_resident::measure_device_encode_throughput`;
302// older host-only full-path harnesses pass `false`. This benchmark therefore
303// never fabricates a device "batched exact per-row GPU encode" number from a
304// host encode — it reports the full-path timing and a correctness contract
305// (support agreement, coordinate error, reconstruction explained-variance, and
306// fallback rate), while the caller-owned engagement flag decides whether the
307// #988 deployment/surrogate gate may consume the rate as a device measurement.
308// ===========================================================================
309
310/// End-to-end throughput of the FULL exact per-row encode for one batch.
311///
312/// Distinct from [`ResidentSolveThroughput`] (which times only the inner solve):
313/// `rows_per_sec` here is `n_rows / encode_secs` for the *entire* production
314/// `certified_encode_batch` — routing, per-row Newton, certificate, fallback,
315/// and the per-row reconstruction selection included.
316#[derive(Clone, Copy, Debug)]
317pub struct FullEncodeThroughput {
318    /// Rows encoded in the timed batch.
319    pub n_rows: usize,
320    /// Wall-clock seconds for the full encode of the batch.
321    pub encode_secs: f64,
322    /// `n_rows / encode_secs` (`0.0` for a degenerate / non-positive time).
323    pub rows_per_sec: f64,
324    /// `true` ONLY if a device-resident exact-encode kernel actually ran the
325    /// encode. No such kernel exists yet, so this is `false` even on a GPU host
326    /// — the flag is the false-routing guard that keeps the CPU encode rate from
327    /// ever being reported as a device measurement.
328    pub device_encode_engaged: bool,
329}
330
331impl FullEncodeThroughput {
332    /// Build a throughput record from a measured elapsed time. `engaged` is the
333    /// caller's honest assertion that a device-resident encode kernel produced
334    /// the result; pass `false` for the host encode path.
335    #[must_use]
336    pub fn from_elapsed(n_rows: usize, elapsed: Duration, device_encode_engaged: bool) -> Self {
337        let encode_secs = elapsed.as_secs_f64();
338        let rows_per_sec = if n_rows > 0 && encode_secs > 0.0 {
339            n_rows as f64 / encode_secs
340        } else {
341            0.0
342        };
343        Self {
344            n_rows,
345            encode_secs,
346            rows_per_sec,
347            device_encode_engaged,
348        }
349    }
350}
351
352/// Correctness of an encode result, measured against the production CPU encode
353/// (a per-row reference) and the reconstruction it implies.
354///
355/// Every field is a quantity a "batched exact per-row encode" claim has to
356/// stand on: it must AGREE with the production per-row encode (support +
357/// coordinates), it must RECONSTRUCT the targets (explained variance), and it
358/// must be honest about how many rows it could not certify (fallback rate).
359#[derive(Clone, Copy, Debug)]
360pub struct EncodeQualityMetrics {
361    /// Rows compared.
362    pub n_rows: usize,
363    /// Rows the encode-under-test certified (`h ≤ ½`, exact-into-the-ball).
364    pub certified_rows: usize,
365    /// Fraction of rows the encode-under-test could NOT certify and flagged for
366    /// the multi-start fallback (`1 - certified_rows/n_rows`). This is the
367    /// "fallback rate".
368    pub fallback_rate: f64,
369    /// Fraction of rows whose certificate flag AGREES with the per-row reference
370    /// encode. For a correct batched encode this is `1.0` (the batch is just the
371    /// per-row encode fanned out).
372    pub support_agreement: f64,
373    /// Largest absolute latent-coordinate difference between the encode-under-test
374    /// and the per-row reference encode, over all rows and coordinate dims. A
375    /// correct batched encode matches the per-row encode to round-off (≈ `0`).
376    pub max_coord_abs_err: f64,
377    /// Largest absolute element-wise reconstruction residual `|x̂ − x|` over the
378    /// whole batch (the "amplitude"/reconstruction error in raw output units).
379    pub max_reconstruction_abs_err: f64,
380    /// Reconstruction explained variance `1 − ‖X − X̂‖²_F / ‖X − X̄‖²_F`, with each
381    /// output column centered by its own mean `X̄`. `1.0` is a perfect on-manifold
382    /// reconstruction; `0.0` is no better than the per-column mean.
383    pub reconstruction_ev: f64,
384}
385
386/// Compute [`EncodeQualityMetrics`] for an encode result.
387///
388/// * `coords` / `certified` — the encode UNDER TEST (`n×d` coords, `n` flags).
389/// * `coords_ref` / `certified_ref` — the production per-row reference encode
390///   (the definition of truth the batched/accelerated encode must match).
391/// * `reconstruction` — the decoded reconstruction `x̂` implied by `coords`
392///   (`n×p`, i.e. `amplitudeᵢ · Φ(coordsᵢ) · B`).
393/// * `targets` — the encode inputs `x` (`n×p`).
394///
395/// Panics on a shape mismatch: this is a benchmark/correctness helper and a
396/// mismatched comparison would silently launder a wrong number.
397#[must_use]
398pub fn encode_quality_metrics(
399    coords: ArrayView2<'_, f64>,
400    certified: &[bool],
401    coords_ref: ArrayView2<'_, f64>,
402    certified_ref: &[bool],
403    reconstruction: ArrayView2<'_, f64>,
404    targets: ArrayView2<'_, f64>,
405) -> EncodeQualityMetrics {
406    let (n, d) = coords.dim();
407    assert_eq!(
408        coords_ref.dim(),
409        (n, d),
410        "encode_quality_metrics: reference coords shape {:?} != under-test {:?}",
411        coords_ref.dim(),
412        (n, d)
413    );
414    assert_eq!(
415        certified.len(),
416        n,
417        "certified flags must have one entry per row"
418    );
419    assert_eq!(
420        certified_ref.len(),
421        n,
422        "reference certified flags must have one entry per row"
423    );
424    let (nt, p) = targets.dim();
425    assert_eq!(nt, n, "targets must have one row per encoded row");
426    assert_eq!(
427        reconstruction.dim(),
428        (n, p),
429        "reconstruction shape {:?} != targets {:?}",
430        reconstruction.dim(),
431        (n, p)
432    );
433
434    let certified_rows = certified.iter().filter(|c| **c).count();
435    let fallback_rate = if n > 0 {
436        1.0 - certified_rows as f64 / n as f64
437    } else {
438        0.0
439    };
440
441    let agree = certified
442        .iter()
443        .zip(certified_ref.iter())
444        .filter(|(a, b)| a == b)
445        .count();
446    let support_agreement = if n > 0 { agree as f64 / n as f64 } else { 1.0 };
447
448    let mut max_coord_abs_err = 0.0_f64;
449    for i in 0..n {
450        for j in 0..d {
451            max_coord_abs_err = max_coord_abs_err.max((coords[[i, j]] - coords_ref[[i, j]]).abs());
452        }
453    }
454
455    // Reconstruction error + explained variance (per-column centering).
456    let mut max_reconstruction_abs_err = 0.0_f64;
457    let mut ss_res = 0.0_f64;
458    let mut ss_tot = 0.0_f64;
459    for c in 0..p {
460        let mut mean = 0.0_f64;
461        for i in 0..n {
462            mean += targets[[i, c]];
463        }
464        if n > 0 {
465            mean /= n as f64;
466        }
467        for i in 0..n {
468            let resid = reconstruction[[i, c]] - targets[[i, c]];
469            max_reconstruction_abs_err = max_reconstruction_abs_err.max(resid.abs());
470            ss_res += resid * resid;
471            let centered = targets[[i, c]] - mean;
472            ss_tot += centered * centered;
473        }
474    }
475    let reconstruction_ev = if ss_tot > 0.0 {
476        1.0 - ss_res / ss_tot
477    } else {
478        // Degenerate (all targets equal their column mean): a perfect
479        // reconstruction is EV 1, anything else is 0 rather than a NaN.
480        if ss_res == 0.0 { 1.0 } else { 0.0 }
481    };
482
483    EncodeQualityMetrics {
484        n_rows: n,
485        certified_rows,
486        fallback_rate,
487        support_agreement,
488        max_coord_abs_err,
489        max_reconstruction_abs_err,
490        reconstruction_ev,
491    }
492}
493
494#[cfg(test)]
495mod full_encode_metric_tests {
496    use super::*;
497    use ndarray::array;
498
499    #[test]
500    fn throughput_is_rows_over_seconds_and_guards_degenerate_time() {
501        let t = FullEncodeThroughput::from_elapsed(8_000, Duration::from_millis(100), false);
502        assert_eq!(t.n_rows, 8_000);
503        assert!(!t.device_encode_engaged);
504        // 8000 rows / 0.1 s = 80_000 rows/sec.
505        assert!(
506            (t.rows_per_sec - 80_000.0).abs() < 1.0,
507            "got {}",
508            t.rows_per_sec
509        );
510        // Zero elapsed is a non-measurement, not an infinite rate.
511        let z = FullEncodeThroughput::from_elapsed(8_000, Duration::ZERO, false);
512        assert_eq!(z.rows_per_sec, 0.0);
513    }
514
515    #[test]
516    fn perfect_match_scores_full_agreement_and_unit_ev() {
517        // Two rows, 1 latent dim, 2 output dims. Reconstruction == targets.
518        let coords = array![[0.10], [0.40]];
519        let targets = array![[1.0, 0.0], [0.0, 1.0]];
520        let m = encode_quality_metrics(
521            coords.view(),
522            &[true, true],
523            coords.view(),
524            &[true, true],
525            targets.view(),
526            targets.view(),
527        );
528        assert_eq!(m.n_rows, 2);
529        assert_eq!(m.certified_rows, 2);
530        assert_eq!(m.fallback_rate, 0.0);
531        assert_eq!(m.support_agreement, 1.0);
532        assert_eq!(m.max_coord_abs_err, 0.0);
533        assert_eq!(m.max_reconstruction_abs_err, 0.0);
534        assert!((m.reconstruction_ev - 1.0).abs() < 1e-12);
535    }
536
537    #[test]
538    fn divergence_is_surfaced_in_every_axis() {
539        let coords = array![[0.10], [0.40]];
540        let coords_ref = array![[0.10], [0.50]]; // row 1 differs by 0.10
541        let targets = array![[1.0, 0.0], [0.0, 1.0]];
542        // Reconstruction misses target by 0.25 on one element.
543        let recon = array![[1.0, 0.0], [0.0, 0.75]];
544        let m = encode_quality_metrics(
545            coords.view(),
546            &[true, false], // row 1 uncertified under test
547            coords_ref.view(),
548            &[true, true], // reference certified both
549            recon.view(),
550            targets.view(),
551        );
552        assert_eq!(m.certified_rows, 1);
553        assert!((m.fallback_rate - 0.5).abs() < 1e-12);
554        assert!((m.support_agreement - 0.5).abs() < 1e-12); // row 1 flags disagree
555        assert!((m.max_coord_abs_err - 0.10).abs() < 1e-12);
556        assert!((m.max_reconstruction_abs_err - 0.25).abs() < 1e-12);
557        assert!(m.reconstruction_ev < 1.0);
558    }
559}