Skip to main content

gam_models/bms/gpu/
row.rs

1//! Stage 2 of the BMS FLEX row kernel — per-row math that turns per-cell
2//! derivative moments (built by Stage 1 in `src/gpu/cubic_cell/mod.rs`) into a
3//! row gradient and row-primary `r × r` Hessian.
4//!
5//! Math (mirrors the CPU reference
6//! `BernoulliMarginalSlope::lower_bms_flex_row_order2_from_parts` in
7//! `src/families/bernoulli_marginal_slope.rs`):
8//!
9//! For each row `i`, with per-cell cubic predictor coefficients
10//! `C_c = (C0, C1, C2, C3)` and derivative moments `m_0..m_9`, build
11//!
12//! ```text
13//!     κ        = 1 / (2π)
14//!     T_n      = κ · Σ_{e=0..3} C_e · m_{e+n}     (n = 0..6)
15//!     D(R)     = κ · Σ_{k=0..3} R_k · m_k
16//!     Q(R, S)  = Σ_{p,q=0..3} R_p · S_q · T_{p+q}
17//!     H(R, S, U) = D(U) − Q(R, S)
18//! ```
19//!
20//! Per cell `c`, accumulate into row scratch:
21//!
22//! ```text
23//!     F_a   += D(A_c)
24//!     F_aa  += H(A_c, A_c, AA_c)
25//!     F_u   += D(R_{c,u})                         u > 0
26//!     F_au  += H(A_c, R_{c,u}, AR_{c,u})          u > 0
27//!     F_uv  += H(R_{c,u}, R_{c,v}, S_{c,uv})      0 < u ≤ v
28//! ```
29//!
30//! After the cell sum, the `q`-row is overridden:
31//!
32//! ```text
33//!     F_q  = −mu_1
34//!     F_qq = −mu_2
35//!     F_qv = 0   (v > 0)
36//!     F_aq = 0
37//! ```
38//!
39//! Implicit function theorem (single `1/F_a`):
40//!
41//! ```text
42//!     inv_Fa = 1 / F_a
43//!     a_u    = −F_u · inv_Fa                       (q-row override: mu_1 · inv_Fa)
44//!     a_uv   = −(F_uv + F_au·a_v + F_av·a_u + F_aa·a_u·a_v) · inv_Fa
45//! ```
46//!
47//! Observed predictor at `z_obs` (host supplies pre-evaluated chi, xi, rho, tau,
48//! r_uv per row and coordinate):
49//!
50//! ```text
51//!     bar_e_u  = chi_obs · a_u + rho_u
52//!     bar_e_uv = chi_obs · a_uv + xi_obs · a_u · a_v + tau_u · a_v
53//!                + a_u · tau_v + r_uv
54//! ```
55//!
56//! Probit Mills (stable; uses the shared curvature primitive from
57//! `numerics_device::PROBIT_NUMERICS_CU`):
58//!
59//! ```text
60//!     s = 2y − 1 ;  m = s · e_obs
61//!     [log_cdf, λ, C] = log_ndtr_mills_curvature(m),  C = −d²logΦ(m)/dm²
62//!     A = −w · s · λ
63//!     B =  w · C
64//! ```
65//!
66//! Final outputs:
67//!
68//! ```text
69//!     neglog   = −w · log_cdf
70//!     g_u      = A · bar_e_u
71//!     H_{uv}   = B · bar_e_u · bar_e_v + A · bar_e_uv     (symmetric)
72//! ```
73//!
74//! Implementation choice (Stage 2): **one CUDA block per row**, with
75//! `blockDim.x = 32` threads. The already-required output buffers are the
76//! width-general scratch authority: `out_grad[row]` evolves `F_u → a_u → g`,
77//! and `out_hess[row]` evolves `F_uv → a_uv → H`. One additional checked
78//! `[n, r]` buffer holds `F_au`. Only the fixed 32-thread scalar reduction is
79//! shared, so primary width has no shared-memory or thread-stack ceiling.
80
81#[cfg(target_os = "linux")]
82use std::sync::OnceLock;
83
84use gam_gpu::gpu_error::GpuError;
85
86#[cfg(target_os = "linux")]
87use std::sync::Arc;
88
89#[cfg(target_os = "linux")]
90use cudarc::driver::{CudaModule, CudaSlice, CudaStream, LaunchConfig, PushKernelArg};
91
92#[cfg(target_os = "linux")]
93use super::super::flex_row_program::{
94    BmsFlexCalibrationOrder2Phase, BmsFlexRowOrder2FinalizerPhase, BmsFlexRowProgram,
95};
96
97/// `blockDim.x` for the row kernel. Threads of a row-block parallelise the
98/// per-cell loop; thread 0 of the block finalises the IFT solve. Linux-only
99/// because the kernel launcher that consumes it is Linux-only.
100#[cfg(target_os = "linux")]
101pub(crate) const ROW_KERNEL_THREADS: u32 = 32;
102
103/// Number of cubic predictor coefficients per cell (`C0..C3`) and the matching
104/// support length of `A_c`, `R_{c,u}`, `AA_c`, `AR_{c,u}`, `S_{c,uv}`.
105pub(crate) const COEFF4: usize = 4;
106
107/// Highest moment index touched per cell: `T_n` uses `m_{n+e}` for `e = 0..3`
108/// and `n = 0..6`, so the maximum index is `9`. `MOMENT_STRIDE = 10`.
109pub(crate) const MOMENT_STRIDE: usize = 10;
110
111/// Source of the per-cell derivative moments fed into the row kernel.
112/// Phase-4 wiring: the substrate at `src/gpu/cubic_cell/mod.rs` can produce
113/// these on the GPU; this enum lets the launcher consume them directly
114/// without a DtoH+HtoD round-trip.
115pub(crate) enum CellMomentsSource<'a> {
116    /// Host-resident `[total_cells, MOMENT_STRIDE = 10]` row-major buffer.
117    /// The launcher will HtoD-upload this on every launch.
118    Host(&'a [f64]),
119    /// Device-resident moments already living on the row-kernel backend's
120    /// default stream (which is the same `cuda_context_for(ordinal).default_stream()`
121    /// the cubic-cell substrate uses, so no cross-context copy is needed).
122    /// Length on the device must be `total_cells * MOMENT_STRIDE`. Linux-only.
123    #[cfg(target_os = "linux")]
124    Device(&'a CudaSlice<f64>),
125}
126
127impl<'a> CellMomentsSource<'a> {
128    /// Logical element count of the moments source, used by [`BmsFlexRowKernelInputs::validate`].
129    pub(crate) fn len(&self) -> usize {
130        match self {
131            CellMomentsSource::Host(slice) => slice.len(),
132            #[cfg(target_os = "linux")]
133            CellMomentsSource::Device(d) => d.len(),
134        }
135    }
136}
137
138/// Per-row input bundle for [`launch_bms_flex_row_kernel`].
139///
140/// Coordinate ordering convention: `u = 0` is `a` (the latent intercept and
141/// the variable IFT eliminates); `u = 1` is `b` (slope); `u = 2..2+p_h` is the
142/// score-warp `β_h` block; `u = 2+p_h..2+p_h+p_w` is the link-wiggle `β_w`
143/// block. So `r = 2 + p_h + p_w` and `u = 1` is the `b` (slope) index used by
144/// the sparse `S_{b·h}` / `S_{b·w}` payloads.
145macro_rules! define_bms_flex_row_kernel_input_types {
146    (
147        f64_fields: [$($f64_field:ident),+ $(,)?],
148        u32_fields: [$($u32_field:ident),+ $(,)?],
149        moments_field: $moments_field:ident $(,)?
150    ) => {
151        pub(crate) struct BmsFlexRowKernelInputs<'a> {
152            /// Number of observation rows.
153            pub n_rows: usize,
154            /// Total primary local dimension. `r = 2 + p_h + p_w`.
155            pub r: usize,
156            /// Number of score-warp basis coordinates.
157            pub p_h: usize,
158            /// Number of link-wiggle basis coordinates.
159            pub p_w: usize,
160            /// Probit frailty scale `S_f` (scalar shared across rows; matches
161            /// `BernoulliMarginalSlope::probit_frailty_scale`).
162            pub s_f: f64,
163            $(pub $f64_field: &'a [f64],)+
164            $(pub $u32_field: &'a [u32],)+
165            pub $moments_field: CellMomentsSource<'a>,
166        }
167
168        /// Owned twin of [`BmsFlexRowKernelInputs`] — every borrowed slice is
169        /// replaced by an owned `Vec`. The buffer fields are declared from the
170        /// same schema as the borrowed launch ABI and converted by
171        /// [`BmsFlexRowKernelInputsOwned::as_borrowed`].
172        pub(crate) struct BmsFlexRowKernelInputsOwned {
173            pub n_rows: usize,
174            pub r: usize,
175            pub p_h: usize,
176            pub p_w: usize,
177            pub s_f: f64,
178            $(pub $f64_field: Vec<f64>,)+
179            $(pub $u32_field: Vec<u32>,)+
180            pub $moments_field: Vec<f64>,
181            /// Phase-4 device-resident moments. When `Some(_)`, the launcher
182            /// skips the host upload and consumes the buffer directly.
183            /// Linux-only field.
184            #[cfg(target_os = "linux")]
185            pub cell_moments_device: Option<CudaSlice<f64>>,
186        }
187
188        impl BmsFlexRowKernelInputsOwned {
189            /// Borrowed view over `self` suitable for
190            /// [`launch_bms_flex_row_kernel`]. The returned struct holds
191            /// references into `self` so the owned bundle must outlive the
192            /// launch.
193            pub(crate) fn as_borrowed(&self) -> BmsFlexRowKernelInputs<'_> {
194                #[cfg(target_os = "linux")]
195                let cell_moments = match self.cell_moments_device.as_ref() {
196                    Some(d) => CellMomentsSource::Device(d),
197                    None => CellMomentsSource::Host(&self.cell_moments),
198                };
199                #[cfg(not(target_os = "linux"))]
200                let cell_moments = CellMomentsSource::Host(&self.cell_moments);
201                BmsFlexRowKernelInputs {
202                    n_rows: self.n_rows,
203                    r: self.r,
204                    p_h: self.p_h,
205                    p_w: self.p_w,
206                    s_f: self.s_f,
207                    $($f64_field: &self.$f64_field,)+
208                    $($u32_field: &self.$u32_field,)+
209                    $moments_field: cell_moments,
210                }
211            }
212        }
213    };
214}
215
216define_bms_flex_row_kernel_input_types! {
217    f64_fields: [
218        q,
219        b,
220        mu_1,
221        mu_2,
222        z_obs,
223        y,
224        w,
225        e_obs,
226        cell_c0,
227        cell_c1,
228        cell_c2,
229        cell_c3,
230        cell_a,
231        cell_aa,
232        cell_r,
233        cell_ar,
234        cell_sbb,
235        cell_sbh,
236        cell_sbw,
237        chi_obs,
238        xi_obs,
239        rho_u,
240        tau_u,
241        r_uv,
242    ],
243    u32_fields: [cell_offsets],
244    moments_field: cell_moments,
245}
246
247/// Per-row outputs produced by [`launch_bms_flex_row_kernel`].
248#[derive(Debug)]
249pub(crate) struct BmsFlexRowKernelOutputs {
250    /// Per-row negative log-likelihood. Length `n_rows`.
251    pub neglog: Vec<f64>,
252    /// Per-row gradient, row-major `[n_rows, r]`.
253    pub grad: Vec<f64>,
254    /// Per-row Hessian, row-major `[n_rows, r*r]`. The kernel writes the full
255    /// symmetric matrix.
256    pub hess: Vec<f64>,
257}
258
259fn checked_shape_len(context: &str, dimensions: &[usize]) -> Result<usize, GpuError> {
260    dimensions
261        .iter()
262        .copied()
263        .try_fold(1_usize, |product, dimension| {
264            product
265                .checked_mul(dimension)
266                .ok_or_else(|| GpuError::DriverCallFailed {
267                    reason: format!(
268                        "bms_flex_row {context}: shape product overflow for dimensions {dimensions:?}"
269                    ),
270                })
271        })
272}
273
274impl<'a> BmsFlexRowKernelInputs<'a> {
275    /// Sanity-check every shape the kernel relies on. This is the only place
276    /// length errors are surfaced — the device kernel assumes valid layout.
277    pub(crate) fn validate(&self) -> Result<(), GpuError> {
278        if self.n_rows == 0 {
279            return Err(GpuError::DriverCallFailed {
280                reason: "bms_flex_row inputs: n_rows must be > 0".to_string(),
281            });
282        }
283        if self.r == 0 {
284            return Err(GpuError::DriverCallFailed {
285                reason: "bms_flex_row inputs: r must be > 0".to_string(),
286            });
287        }
288        let decomposed_r = 2_usize
289            .checked_add(self.p_h)
290            .and_then(|value| value.checked_add(self.p_w))
291            .ok_or_else(|| GpuError::DriverCallFailed {
292                reason: format!(
293                    "bms_flex_row inputs: primary decomposition overflow for p_h={} p_w={}",
294                    self.p_h, self.p_w
295                ),
296            })?;
297        if self.r != decomposed_r {
298            return Err(GpuError::DriverCallFailed {
299                reason: format!(
300                    "bms_flex_row inputs: r={} must equal 2 + p_h({}) + p_w({}) = {}",
301                    self.r, self.p_h, self.p_w, decomposed_r
302                ),
303            });
304        }
305        let n = self.n_rows;
306        let check_len = |name: &str, have: usize, want: usize| -> Result<(), GpuError> {
307            if have != want {
308                return Err(GpuError::DriverCallFailed {
309                    reason: format!("bms_flex_row inputs: {name}.len()={have} != {want}"),
310                });
311            }
312            Ok(())
313        };
314        check_len("q", self.q.len(), n)?;
315        check_len("b", self.b.len(), n)?;
316        check_len("mu_1", self.mu_1.len(), n)?;
317        check_len("mu_2", self.mu_2.len(), n)?;
318        check_len("z_obs", self.z_obs.len(), n)?;
319        check_len("y", self.y.len(), n)?;
320        check_len("w", self.w.len(), n)?;
321        check_len("e_obs", self.e_obs.len(), n)?;
322        check_len("chi_obs", self.chi_obs.len(), n)?;
323        check_len("xi_obs", self.xi_obs.len(), n)?;
324        let nr = checked_shape_len("validate [n,r]", &[n, self.r])?;
325        let nrr = checked_shape_len("validate [n,r,r]", &[n, self.r, self.r])?;
326        check_len("rho_u", self.rho_u.len(), nr)?;
327        check_len("tau_u", self.tau_u.len(), nr)?;
328        check_len("r_uv", self.r_uv.len(), nrr)?;
329        let offsets_len = n.checked_add(1).ok_or_else(|| GpuError::DriverCallFailed {
330            reason: format!("bms_flex_row inputs: n_rows={n} cannot form n+1 offsets"),
331        })?;
332        check_len("cell_offsets", self.cell_offsets.len(), offsets_len)?;
333        let total_cells_u32 = self.cell_offsets[n];
334        let total_cells = total_cells_u32 as usize;
335        check_len("cell_c0", self.cell_c0.len(), total_cells)?;
336        check_len("cell_c1", self.cell_c1.len(), total_cells)?;
337        check_len("cell_c2", self.cell_c2.len(), total_cells)?;
338        check_len("cell_c3", self.cell_c3.len(), total_cells)?;
339        let cells_coeff4 = checked_shape_len("validate cell coeff4", &[total_cells, COEFF4])?;
340        check_len("cell_a", self.cell_a.len(), cells_coeff4)?;
341        check_len("cell_aa", self.cell_aa.len(), cells_coeff4)?;
342        check_len(
343            "cell_r",
344            self.cell_r.len(),
345            checked_shape_len(
346                "validate cell_r",
347                &[total_cells, self.r.saturating_sub(1), COEFF4],
348            )?,
349        )?;
350        check_len(
351            "cell_ar",
352            self.cell_ar.len(),
353            checked_shape_len(
354                "validate cell_ar",
355                &[total_cells, self.r.saturating_sub(1), COEFF4],
356            )?,
357        )?;
358        check_len("cell_sbb", self.cell_sbb.len(), cells_coeff4)?;
359        check_len(
360            "cell_sbh",
361            self.cell_sbh.len(),
362            checked_shape_len("validate cell_sbh", &[total_cells, self.p_h, COEFF4])?,
363        )?;
364        check_len(
365            "cell_sbw",
366            self.cell_sbw.len(),
367            checked_shape_len("validate cell_sbw", &[total_cells, self.p_w, COEFF4])?,
368        )?;
369        check_len(
370            "cell_moments",
371            self.cell_moments.len(),
372            checked_shape_len("validate cell_moments", &[total_cells, MOMENT_STRIDE])?,
373        )?;
374        // Bonus: when the moments came from `CellMomentsSource::Device`, the
375        // launcher needs to know the source is from a device buffer; nothing
376        // to validate beyond length above. The Host variant length check is
377        // also already covered above.
378        // Monotone cell_offsets check.
379        for i in 0..n {
380            if self.cell_offsets[i] > self.cell_offsets[i + 1] {
381                return Err(GpuError::DriverCallFailed {
382                    reason: format!(
383                        "bms_flex_row inputs: cell_offsets must be monotone (offset[{}]={} > offset[{}]={})",
384                        i,
385                        self.cell_offsets[i],
386                        i + 1,
387                        self.cell_offsets[i + 1]
388                    ),
389                });
390            }
391        }
392        Ok(())
393    }
394}
395
396/// Non-semantic CUDA scaffolding for the generated row kernel. One CUDA block
397/// per row; the generated launch width parallelises the per-cell sums into
398/// shared-memory scratch. The calibration and finalizer markers are replaced
399/// by interpreting [`BmsFlexRowProgram`]'s typed node streams.
400///
401/// Shared probit numerics (`erfcx_nonnegative`, `log_ndtr`,
402/// `log_ndtr_and_mills`) are provided by
403/// `numerics_device::PROBIT_NUMERICS_CU`, which is prepended before
404/// passing to `cudarc::nvrtc::compile_ptx`.
405///
406#[cfg(target_os = "linux")]
407const CUDA_ROW_KERNEL_TEMPLATE: &str = r#"
408// One block per row. threadIdx.x parallelises per-cell sums.
409// Semantic calibration/finalization visits are generated from BmsFlexRowProgram.
410
411#define INV_TWO_PI     0.15915494309189535
412#define BMS_FLEX_ROW_THREADS /*__BMS_FLEX_ROW_THREADS__*/
413
414extern "C" __device__ __forceinline__ double atomic_add_f64(double *addr, double value) {
415    unsigned long long int *addr_as_ull = (unsigned long long int *)addr;
416    unsigned long long int old = *addr_as_ull;
417    unsigned long long int assumed;
418    do {
419        assumed = old;
420        double next = __longlong_as_double((long long int)assumed) + value;
421        old = atomicCAS(addr_as_ull, assumed, (unsigned long long int)__double_as_longlong(next));
422    } while (assumed != old);
423    return __longlong_as_double((long long int)old);
424}
425
426// `nan_fill_outputs`: thread-0-only path used when row inputs are degenerate
427// (`F_a` non-finite or non-positive). The status channel makes the host reject
428// the entire selected-GPU execution before any output can enter a cache.
429extern "C" __device__ __forceinline__ void
430nan_fill_outputs(int r,
431                 int row,
432                 double *out_neglog,
433                 double *out_grad,
434                 double *out_hess,
435                 unsigned int *out_status) {
436    double nan_value = __longlong_as_double(0x7ff8000000000000ULL);
437    out_status[row] = 1U;
438    out_neglog[row] = nan_value;
439    size_t row_r = (size_t)row * (size_t)r;
440    for (int u = 0; u < r; ++u) {
441        out_grad[row_r + (size_t)u] = nan_value;
442    }
443    size_t rr = (size_t)r * (size_t)r;
444    size_t row_rr = (size_t)row * rr;
445    for (size_t idx = 0; idx < rr; ++idx) {
446        out_hess[row_rr + idx] = nan_value;
447    }
448}
449
450extern "C" __global__ void bms_flex_row_kernel(
451    int                  n_rows,
452    int                  r,
453    int                  p_h,
454    int                  p_w,
455    double               s_f,                // currently unused on device:
456                                             // host has already baked S_f
457                                             // into the cubic coefficients.
458                                             // Kept for diagnostic parity.
459    const double * __restrict__ row_q,
460    const double * __restrict__ row_b,
461    const double * __restrict__ row_mu1,
462    const double * __restrict__ row_mu2,
463    const double * __restrict__ row_zobs,
464    const double * __restrict__ row_y,
465    const double * __restrict__ row_w,
466    const unsigned int * __restrict__ cell_offsets,
467    const double * __restrict__ cell_c0,
468    const double * __restrict__ cell_c1,
469    const double * __restrict__ cell_c2,
470    const double * __restrict__ cell_c3,
471    const double * __restrict__ cell_a,       // [n_cells, 4]
472    const double * __restrict__ cell_aa,      // [n_cells, 4]
473    const double * __restrict__ cell_r,       // [n_cells, r-1, 4]
474    const double * __restrict__ cell_ar,      // [n_cells, r-1, 4]
475    const double * __restrict__ cell_sbb,     // [n_cells, 4]
476    const double * __restrict__ cell_sbh,     // [n_cells, p_h, 4]
477    const double * __restrict__ cell_sbw,     // [n_cells, p_w, 4]
478    const double * __restrict__ cell_moments, // [n_cells, 10]
479    const double * __restrict__ row_chi,
480    const double * __restrict__ row_xi,
481    const double * __restrict__ row_rho,      // [n_rows, r]
482    const double * __restrict__ row_tau,      // [n_rows, r]
483    const double * __restrict__ row_ruv,      // [n_rows, r*r]
484    const double * __restrict__ row_e_obs,    // [n_rows] observed predictor VALUE
485    double       * __restrict__ row_f_au,      // [n_rows, r] general-width scratch
486    double       * __restrict__ out_neglog,
487    double       * __restrict__ out_grad,
488    double       * __restrict__ out_hess,
489    unsigned int * __restrict__ out_status)
490{
491    int row = blockIdx.x;
492    if (row >= n_rows) return;
493    int tid = threadIdx.x;
494
495    // Width-general row scratch. Reuse the final output allocations in-place:
496    // F_u → a_u → gradient and F_uv → a_uv → Hessian. Only F_au needs one
497    // additional checked [n,r] device allocation.
498    size_t row_r_base = (size_t)row * (size_t)r;
499    size_t rr = (size_t)r * (size_t)r;
500    size_t row_rr_base = (size_t)row * rr;
501    double *F_u = out_grad + row_r_base;
502    double *F_au = row_f_au + row_r_base;
503    double *F_uv = out_hess + row_rr_base;
504    __shared__ double reduce_a[BMS_FLEX_ROW_THREADS];
505    __shared__ double reduce_b[BMS_FLEX_ROW_THREADS];
506    __shared__ double F_a_shared;
507    __shared__ double F_aa_shared;
508
509    // Zero scratch.
510    if (tid == 0) { F_a_shared = 0.0; F_aa_shared = 0.0; }
511    for (int u = tid; u < r; u += blockDim.x) {
512        F_u[u]  = 0.0;
513        F_au[u] = 0.0;
514    }
515    for (size_t uv = (size_t)tid; uv < rr; uv += (size_t)blockDim.x) {
516        F_uv[uv] = 0.0;
517    }
518    __syncthreads();
519
520    // ── per-cell sweep ───────────────────────────────────────────────────
521    unsigned int cell_lo = cell_offsets[row];
522    unsigned int cell_hi = cell_offsets[row + 1];
523    unsigned int n_cells = cell_hi - cell_lo;
524
525    double local_Fa  = 0.0;
526    double local_Faa = 0.0;
527
528    for (unsigned int local_c = (unsigned int)tid;
529         local_c < n_cells;
530         local_c += (unsigned int)blockDim.x) {
531        unsigned int c = cell_lo + local_c;
532
533        // Load cubic predictor coeffs C0..C3.
534        double C[4];
535        C[0] = cell_c0[c]; C[1] = cell_c1[c];
536        C[2] = cell_c2[c]; C[3] = cell_c3[c];
537
538        // Load m_0..m_9.
539        const double *m = cell_moments + (size_t)c * 10;
540
541        // T_n = κ · Σ_e C_e · m_{e+n}, n = 0..6.
542        // CPU parity: equivalent to the `eta_rs ⊗ moments` contraction in
543        //             `cell_second_derivative_from_moments` after folding the
544        //             cubic predictor.
545        double T[7];
546        #pragma unroll
547        for (int n = 0; n < 7; ++n) {
548            double acc = 0.0;
549            #pragma unroll
550            for (int e = 0; e < 4; ++e) {
551                acc = fma(C[e], m[e + n], acc);
552            }
553            T[n] = acc * INV_TWO_PI;
554        }
555
556        // D(R) = κ · Σ_k R_k · m_k.
557        // CPU parity: `cell_first_derivative_from_moments`.
558        // The argument is parenthesized because callers pass pointer
559        // ARITHMETIC (`D_OF(base + offset)`): without it the expansion binds
560        // as `base + offset[0]`, which NVRTC rejects ("pointer-to-object
561        // type" on the integer term) — the calibration-phase emitter was the
562        // first caller to hit this.
563        #define D_OF(R) (INV_TWO_PI * ((R)[0]*m[0] + (R)[1]*m[1] + (R)[2]*m[2] + (R)[3]*m[3]))
564
565        // Q(R, S) = Σ_{p,q} R_p · S_q · T_{p+q}.
566        // CPU parity: the `eta_rs` folded dot in
567        // `cell_second_derivative_from_moments`.
568        #define Q_OF(R, S)                                                                 \
569            (((R)[0]*(S)[0])*T[0] + ((R)[0]*(S)[1] + (R)[1]*(S)[0])*T[1]                   \
570             + ((R)[0]*(S)[2] + (R)[1]*(S)[1] + (R)[2]*(S)[0])*T[2]                        \
571             + ((R)[0]*(S)[3] + (R)[1]*(S)[2] + (R)[2]*(S)[1] + (R)[3]*(S)[0])*T[3]        \
572             + ((R)[1]*(S)[3] + (R)[2]*(S)[2] + (R)[3]*(S)[1])*T[4]                        \
573             + ((R)[2]*(S)[3] + (R)[3]*(S)[2])*T[5]                                        \
574             + ((R)[3]*(S)[3])*T[6])
575
576        // The typed calibration schedule below consumes these primitive
577        // coefficient views through D_OF/Q_OF.
578        const double *A_c  = cell_a  + (size_t)c * 4;
579        const double *AA_c = cell_aa + (size_t)c * 4;
580        /*__BMS_FLEX_CALIBRATION_ORDER2__*/
581
582        #undef D_OF
583        #undef Q_OF
584    }
585
586    // Block reduction of local_Fa, local_Faa into shared.
587    reduce_a[tid] = local_Fa;
588    reduce_b[tid] = local_Faa;
589    __syncthreads();
590    for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
591        if (tid < stride) {
592            reduce_a[tid] += reduce_a[tid + stride];
593            reduce_b[tid] += reduce_b[tid + stride];
594        }
595        __syncthreads();
596    }
597    if (tid == 0) {
598        F_a_shared  = reduce_a[0];
599        F_aa_shared = reduce_b[0];
600    }
601    __syncthreads();
602
603    // ── thread-0 finalisation: IFT + observed-point + Mills + writes ──────
604    if (tid != 0) return;
605
606    double F_a  = F_a_shared;
607    double F_aa = F_aa_shared;
608    double mu_1 = row_mu1[row];
609    double mu_2 = row_mu2[row];
610
611    // q-row overrides.
612    //   F_q  = -mu_1 ; F_qq = -mu_2 ; F_qv = 0 (v > 0) ; F_aq = 0.
613    F_u[0]  = -mu_1;
614    F_au[0] = 0.0;
615    // Zero the q-cross row/column of F_uv (u == 0 or v == 0), then plant -mu_2 at (0,0).
616    for (int v = 0; v < r; ++v) {
617        F_uv[(size_t)v] = 0.0;
618        F_uv[(size_t)v * (size_t)r] = 0.0;
619    }
620    F_uv[0] = -mu_2;
621
622    // Guard: degenerate F_a ⇒ NaN-fill this row's outputs.
623    if (!isfinite(F_a) || F_a <= 0.0) {
624        nan_fill_outputs(r, row, out_neglog, out_grad, out_hess, out_status);
625        return;
626    }
627    double inv_Fa = 1.0 / F_a;
628
629    // Storage consumed by the generated dependency-ordered finalizer. Both
630    // aliases overwrite their no-longer-needed calibration predecessors.
631    double *a_u = F_u;
632    double *a_uv = F_uv;
633    double chi = row_chi[row];
634    double xi  = row_xi[row];
635    const double *rho = row_rho + (size_t)row * r;
636    const double *tau = row_tau + (size_t)row * r;
637    const double *ruv = row_ruv + row_rr_base;
638
639    // Probit Mills.
640    double y    = row_y[row];
641    double w    = row_w[row];
642    double s    = 2.0 * y - 1.0;
643    // The "observed predictor" e_obs is the VALUE (degree-0 term) of the
644    // observed jet η(a(θ), θ; z_obs) — NOT `bar_e_u[0]`, which is the u=0
645    // FIRST-derivative jet (`chi·a_0 + rho_0 = dη_obs/dq`). The host packs
646    // the observed value directly in `row_e_obs[row]` (see
647    // `pack_bms_flex_row_kernel_inputs`, `eta_val = eval_coeff4_at(obs.coeff,
648    // z_obs)`), matching the CPU family `lower_bms_flex_row_order2_from_parts`
649    // which forms `signed_margin = s_y · eta_val`. #415 parity lock.
650    double e_obs = row_e_obs[row];
651    double m_arg = s * e_obs;
652    double log_cdf, lambda, probit_curvature;
653    log_ndtr_mills_curvature(m_arg, &log_cdf, &lambda, &probit_curvature);
654    double A_i = -w * s * lambda;
655    double B_i =  w * probit_curvature;
656
657    out_neglog[row] = -w * log_cdf;
658    /*__BMS_FLEX_ORDER2_FINALIZER__*/
659    if (!isfinite(out_neglog[row])) {
660        out_status[row] = 2U;
661    }
662    for (int u = 0; u < r; ++u) {
663        if (!isfinite(out_grad[row_r_base + (size_t)u])) {
664            out_status[row] = 2U;
665        }
666    }
667    for (size_t uv = 0; uv < rr; ++uv) {
668        if (!isfinite(out_hess[row_rr_base + uv])) {
669            out_status[row] = 2U;
670        }
671    }
672}
673"#;
674
675#[cfg(target_os = "linux")]
676fn build_generated_row_kernel_source() -> String {
677    const CALIBRATION_MARKER: &str = "        /*__BMS_FLEX_CALIBRATION_ORDER2__*/";
678    const FINALIZER_MARKER: &str = "    /*__BMS_FLEX_ORDER2_FINALIZER__*/";
679
680    let (prefix, remainder) = CUDA_ROW_KERNEL_TEMPLATE
681        .split_once(CALIBRATION_MARKER)
682        .expect("CUDA row template must contain the calibration marker");
683    let (between, suffix) = remainder
684        .split_once(FINALIZER_MARKER)
685        .expect("CUDA row template must contain the finalizer marker");
686    let mut source = String::with_capacity(CUDA_ROW_KERNEL_TEMPLATE.len() + 16_000);
687    source.push_str(prefix);
688
689    BmsFlexRowProgram::try_for_each_calibration_order2_phase(
690        true,
691        |phase| -> Result<(), std::convert::Infallible> {
692            source.push_str(&format!(
693                "        // canonical calibration phase: {phase:?}\n"
694            ));
695            match phase {
696                BmsFlexCalibrationOrder2Phase::InterceptFirst => {
697                    source.push_str("        local_Fa += D_OF(A_c);\n");
698                }
699                BmsFlexCalibrationOrder2Phase::InterceptSecond => {
700                    source.push_str("        local_Faa += D_OF(AA_c) - Q_OF(A_c, A_c);\n");
701                }
702                BmsFlexCalibrationOrder2Phase::PrimaryFirstAndInterceptSecond => {
703                    source.push_str(
704                        r#"        for (int u = 1; u < r; ++u) {
705            const double *R_u = cell_r + ((size_t)c * (size_t)(r - 1) + (size_t)(u - 1)) * 4;
706            const double *AR_u = cell_ar + ((size_t)c * (size_t)(r - 1) + (size_t)(u - 1)) * 4;
707            atomic_add_f64(&F_u[u], D_OF(R_u));
708            atomic_add_f64(&F_au[u], D_OF(AR_u) - Q_OF(A_c, R_u));
709        }
710"#,
711                    );
712                }
713                BmsFlexCalibrationOrder2Phase::PrimaryPairSecond => {
714                    source.push_str(
715                        r#"        for (int u = 1; u < r; ++u) {
716            const double *R_u = cell_r + ((size_t)c * (size_t)(r - 1) + (size_t)(u - 1)) * 4;
717            for (int v = u; v < r; ++v) {
718                const double *R_v = cell_r + ((size_t)c * (size_t)(r - 1) + (size_t)(v - 1)) * 4;
719                double explicit_second = 0.0;
720                if (u == 1 && v == 1) {
721                    explicit_second = D_OF(cell_sbb + (size_t)c * 4);
722                } else if (u == 1 && v < 2 + p_h) {
723                    int j = v - 2;
724                    explicit_second = D_OF(cell_sbh + ((size_t)c * (size_t)p_h + (size_t)j) * 4);
725                } else if (u == 1) {
726                    int l = v - (2 + p_h);
727                    explicit_second = D_OF(cell_sbw + ((size_t)c * (size_t)p_w + (size_t)l) * 4);
728                }
729                atomic_add_f64(&F_uv[(size_t)u * (size_t)r + (size_t)v], explicit_second - Q_OF(R_u, R_v));
730            }
731        }
732"#,
733                    );
734                }
735            }
736            Ok(())
737        },
738    )
739    .expect("the infallible calibration phase emitter cannot fail");
740
741    source.push_str(between);
742    BmsFlexRowProgram::try_for_each_order2_finalizer_phase(
743        true,
744        |phase| -> Result<(), std::convert::Infallible> {
745            source.push_str(&format!("    // canonical finalizer phase: {phase:?}\n"));
746            match phase {
747                BmsFlexRowOrder2FinalizerPhase::ImplicitFirst => {
748                    source.push_str(
749                        r#"    for (int u = 0; u < r; ++u) {
750        a_u[u] = -F_u[u] * inv_Fa;
751    }
752"#,
753                    );
754                }
755                BmsFlexRowOrder2FinalizerPhase::ImplicitFirstComplete => {
756                    source.push_str("    // Canonical implicit-first stage complete.\n");
757                }
758                BmsFlexRowOrder2FinalizerPhase::ImplicitSecond => {
759                    source.push_str(
760                        r#"    for (int u = 0; u < r; ++u) {
761        for (int v = u; v < r; ++v) {
762            size_t uv = (size_t)u * (size_t)r + (size_t)v;
763            size_t vu = (size_t)v * (size_t)r + (size_t)u;
764            double term = F_uv[uv]
765                        + F_au[v] * a_u[u]
766                        + F_au[u] * a_u[v]
767                        + F_aa * a_u[u] * a_u[v];
768            double value = -term * inv_Fa;
769            a_uv[uv] = value;
770            a_uv[vu] = value;
771        }
772    }
773"#,
774                    );
775                }
776                BmsFlexRowOrder2FinalizerPhase::ObservedFirst => {
777                    source.push_str("    // Observed first derivatives are derived on demand.\n");
778                }
779                BmsFlexRowOrder2FinalizerPhase::ObservedScoreSensitivity => {
780                    source.push_str(
781                        "    // Score sensitivity has no Stage-2 device output channel.\n",
782                    );
783                }
784                BmsFlexRowOrder2FinalizerPhase::ObservedSecond => {
785                    source.push_str(
786                        r#"    for (int u = 0; u < r; ++u) {
787        for (int v = u; v < r; ++v) {
788            size_t uv = (size_t)u * (size_t)r + (size_t)v;
789            size_t vu = (size_t)v * (size_t)r + (size_t)u;
790            double bar_e_u = chi * a_u[u] + rho[u];
791            double bar_e_v = chi * a_u[v] + rho[v];
792            double observed_second = chi * a_uv[uv]
793                                   + xi * a_u[u] * a_u[v]
794                                   + tau[u] * a_u[v]
795                                   + a_u[u] * tau[v]
796                                   + ruv[uv];
797            double hessian_value =
798                B_i * bar_e_u * bar_e_v + A_i * observed_second;
799            out_hess[row_rr_base + uv] = hessian_value;
800            out_hess[row_rr_base + vu] = hessian_value;
801        }
802    }
803"#,
804                    );
805                }
806                BmsFlexRowOrder2FinalizerPhase::NegLogFirst => {
807                    source.push_str(
808                        r#"    for (int u = 0; u < r; ++u) {
809        double bar_e_u = chi * a_u[u] + rho[u];
810        out_grad[row_r_base + (size_t)u] = A_i * bar_e_u;
811    }
812"#,
813                    );
814                }
815            }
816            Ok(())
817        },
818    )
819    .expect("the infallible finalizer phase emitter cannot fail");
820    source.push_str(suffix);
821    source.replace(
822        "/*__BMS_FLEX_ROW_THREADS__*/",
823        &ROW_KERNEL_THREADS.to_string(),
824    )
825}
826
827#[cfg(target_os = "linux")]
828pub(crate) fn generated_row_kernel_source() -> &'static str {
829    static SOURCE: OnceLock<String> = OnceLock::new();
830    SOURCE.get_or_init(build_generated_row_kernel_source)
831}
832
833// Force `s_f` to be considered used at the Rust level even though Stage 2 of
834// the kernel doesn't consume it on-device (the host has already baked the
835// probit frailty scale into the per-cell cubic coefficients). The dispatcher
836// validates that the host-baked cubic coefficients came from a finite,
837// positive frailty scale; reading it here also avoids a `let _` silencer.
838#[inline]
839pub(crate) fn s_f_diagnostic_finite(inputs: &BmsFlexRowKernelInputs<'_>) -> bool {
840    inputs.s_f.is_finite() && inputs.s_f > 0.0
841}
842
843#[cfg(target_os = "linux")]
844pub(crate) struct RowKernelBackend {
845    pub(crate) stream: Arc<CudaStream>,
846    pub(crate) module: Arc<CudaModule>,
847}
848
849#[cfg(target_os = "linux")]
850impl RowKernelBackend {
851    pub(crate) fn probe() -> Result<&'static Self, GpuError> {
852        static BACKEND: OnceLock<Result<RowKernelBackend, GpuError>> = OnceLock::new();
853        BACKEND
854            .get_or_init(|| {
855                gam_gpu::backend_probe::probe_backend_with_compile("bms_flex_row", |parts| {
856                    let row_kernel_source = [
857                        gam_gpu::numerics_device::PROBIT_NUMERICS_CU,
858                        generated_row_kernel_source(),
859                    ]
860                    .concat();
861                    // #1551: route through the project's single arch-aware NVRTC
862                    // entry point instead of bare `cudarc::nvrtc::compile_ptx`.
863                    // `compile_ptx_arch` pins `--gpu-architecture` to the selected
864                    // device's compute capability and supplies the standard CUDA
865                    // include paths; bare `compile_ptx` uses NVRTC's default
866                    // virtual arch with no includes. The row kernel's 64-bit
867                    // `atomic_add_f64` (atomicCAS emulation) compiles best against
868                    // the real device arch, and this keeps every BMS-flex compile
869                    // site consistent with the SAE arrow/Schur kernels that do
870                    // require the sm_60 pin for native `atomicAdd(double*,double)`.
871                    let ptx = gam_gpu::device_cache::compile_ptx_arch(&row_kernel_source).map_err(
872                        |err| GpuError::DriverCallFailed {
873                            reason: format!("bms_flex_row NVRTC compile failed: {err}"),
874                        },
875                    )?;
876                    let module =
877                        parts
878                            .ctx
879                            .load_module(ptx)
880                            .map_err(|err| GpuError::DriverCallFailed {
881                                reason: format!("bms_flex_row module load failed: {err}"),
882                            })?;
883                    Ok(RowKernelBackend {
884                        stream: parts.stream.clone(),
885                        module,
886                    })
887                })
888            })
889            .as_ref()
890            .map_err(GpuError::clone)
891    }
892}
893
894/// Launch Stage-2 BMS FLEX row kernel. On non-Linux returns
895/// [`GpuError::DriverLibraryUnavailable`]; on Linux NVRTC-compiles the kernel
896/// (cached for the process lifetime), uploads the per-row + per-cell buffers,
897/// and dispatches one block per row.
898pub(crate) fn launch_bms_flex_row_kernel(
899    inputs: BmsFlexRowKernelInputs<'_>,
900) -> Result<BmsFlexRowKernelOutputs, GpuError> {
901    inputs.validate()?;
902    if !s_f_diagnostic_finite(&inputs) {
903        return Err(GpuError::DriverCallFailed {
904            reason: format!(
905                "bms_flex_row inputs: s_f must be positive and finite, got {}",
906                inputs.s_f
907            ),
908        });
909    }
910
911    #[cfg(target_os = "linux")]
912    {
913        launch_linux(inputs)
914    }
915    #[cfg(not(target_os = "linux"))]
916    {
917        Err(GpuError::DriverLibraryUnavailable {
918            reason: "bms_flex_row GPU kernel is Linux-only".to_string(),
919        })
920    }
921}
922
923#[cfg(target_os = "linux")]
924pub(crate) fn launch_linux(
925    inputs: BmsFlexRowKernelInputs<'_>,
926) -> Result<BmsFlexRowKernelOutputs, GpuError> {
927    let backend = RowKernelBackend::probe()?;
928    let stream = &backend.stream;
929
930    let upload_f64 = |slice: &[f64], label: &str| {
931        stream
932            .clone_htod(slice)
933            .map_err(|err| GpuError::DriverCallFailed {
934                reason: format!("bms_flex_row upload {label}: {err}"),
935            })
936    };
937    let upload_u32 = |slice: &[u32], label: &str| {
938        stream
939            .clone_htod(slice)
940            .map_err(|err| GpuError::DriverCallFailed {
941                reason: format!("bms_flex_row upload {label}: {err}"),
942            })
943    };
944
945    let d_q = upload_f64(inputs.q, "q")?;
946    let d_b = upload_f64(inputs.b, "b")?;
947    let d_mu1 = upload_f64(inputs.mu_1, "mu_1")?;
948    let d_mu2 = upload_f64(inputs.mu_2, "mu_2")?;
949    let d_zobs = upload_f64(inputs.z_obs, "z_obs")?;
950    let d_y = upload_f64(inputs.y, "y")?;
951    let d_w = upload_f64(inputs.w, "w")?;
952    let d_offsets = upload_u32(inputs.cell_offsets, "cell_offsets")?;
953    let d_c0 = upload_f64(inputs.cell_c0, "cell_c0")?;
954    let d_c1 = upload_f64(inputs.cell_c1, "cell_c1")?;
955    let d_c2 = upload_f64(inputs.cell_c2, "cell_c2")?;
956    let d_c3 = upload_f64(inputs.cell_c3, "cell_c3")?;
957    let d_a = upload_f64(inputs.cell_a, "cell_a")?;
958    let d_aa = upload_f64(inputs.cell_aa, "cell_aa")?;
959    let d_r = upload_f64(inputs.cell_r, "cell_r")?;
960    let d_ar = upload_f64(inputs.cell_ar, "cell_ar")?;
961    let d_sbb = upload_f64(inputs.cell_sbb, "cell_sbb")?;
962    let d_sbh = upload_f64(inputs.cell_sbh, "cell_sbh")?;
963    let d_sbw = upload_f64(inputs.cell_sbw, "cell_sbw")?;
964    // Phase-4: optionally consume device-resident moments (no host upload).
965    // Both branches end up holding a `&CudaSlice<f64>` named `d_moments_ref`
966    // we can pass to the launch builder uniformly.
967    let owned_host_moments: CudaSlice<f64>;
968    let d_moments_ref: &CudaSlice<f64> = match &inputs.cell_moments {
969        CellMomentsSource::Host(slice) => {
970            owned_host_moments = upload_f64(slice, "cell_moments")?;
971            &owned_host_moments
972        }
973        CellMomentsSource::Device(d) => *d,
974    };
975    let d_chi = upload_f64(inputs.chi_obs, "chi_obs")?;
976    let d_xi = upload_f64(inputs.xi_obs, "xi_obs")?;
977    let d_rho = upload_f64(inputs.rho_u, "rho_u")?;
978    let d_tau = upload_f64(inputs.tau_u, "tau_u")?;
979    let d_ruv = upload_f64(inputs.r_uv, "r_uv")?;
980    let d_e_obs = upload_f64(inputs.e_obs, "e_obs")?;
981
982    let n = inputs.n_rows;
983    let r = inputs.r;
984    let nr = checked_shape_len("launch [n,r]", &[n, r])?;
985    let nrr = checked_shape_len("launch [n,r,r]", &[n, r, r])?;
986    let mut d_neglog = stream
987        .alloc_zeros::<f64>(n)
988        .map_err(|err| GpuError::DriverCallFailed {
989            reason: format!("bms_flex_row alloc neglog: {err}"),
990        })?;
991    let mut d_grad = stream
992        .alloc_zeros::<f64>(nr)
993        .map_err(|err| GpuError::DriverCallFailed {
994            reason: format!("bms_flex_row alloc grad: {err}"),
995        })?;
996    let mut d_hess = stream
997        .alloc_zeros::<f64>(nrr)
998        .map_err(|err| GpuError::DriverCallFailed {
999            reason: format!("bms_flex_row alloc hess: {err}"),
1000        })?;
1001    let mut d_f_au = stream
1002        .alloc_zeros::<f64>(nr)
1003        .map_err(|err| GpuError::DriverCallFailed {
1004            reason: format!("bms_flex_row alloc F_au scratch: {err}"),
1005        })?;
1006    let mut d_status = stream
1007        .alloc_zeros::<u32>(n)
1008        .map_err(|err| GpuError::DriverCallFailed {
1009            reason: format!("bms_flex_row alloc status: {err}"),
1010        })?;
1011
1012    let func = backend
1013        .module
1014        .load_function("bms_flex_row_kernel")
1015        .map_err(|err| GpuError::DriverCallFailed {
1016            reason: format!("bms_flex_row load_function: {err}"),
1017        })?;
1018
1019    let n_u32 = u32::try_from(n).map_err(|_| GpuError::DriverCallFailed {
1020        reason: format!("bms_flex_row: n_rows={n} exceeds CUDA grid range"),
1021    })?;
1022    let cfg = LaunchConfig {
1023        grid_dim: (n_u32, 1, 1),
1024        block_dim: (ROW_KERNEL_THREADS, 1, 1),
1025        shared_mem_bytes: 0,
1026    };
1027    let n_i32 = i32::try_from(n).map_err(|_| GpuError::DriverCallFailed {
1028        reason: format!("bms_flex_row: n_rows={n} exceeds i32 range"),
1029    })?;
1030    let r_i32 = i32::try_from(r).map_err(|_| GpuError::DriverCallFailed {
1031        reason: format!("bms_flex_row: r={r} exceeds i32 range"),
1032    })?;
1033    let p_h_i32 = i32::try_from(inputs.p_h).map_err(|_| GpuError::DriverCallFailed {
1034        reason: format!("bms_flex_row: p_h={} exceeds i32 range", inputs.p_h),
1035    })?;
1036    let p_w_i32 = i32::try_from(inputs.p_w).map_err(|_| GpuError::DriverCallFailed {
1037        reason: format!("bms_flex_row: p_w={} exceeds i32 range", inputs.p_w),
1038    })?;
1039    let s_f = inputs.s_f;
1040
1041    let mut builder = stream.launch_builder(&func);
1042    builder
1043        .arg(&n_i32)
1044        .arg(&r_i32)
1045        .arg(&p_h_i32)
1046        .arg(&p_w_i32)
1047        .arg(&s_f)
1048        .arg(&d_q)
1049        .arg(&d_b)
1050        .arg(&d_mu1)
1051        .arg(&d_mu2)
1052        .arg(&d_zobs)
1053        .arg(&d_y)
1054        .arg(&d_w)
1055        .arg(&d_offsets)
1056        .arg(&d_c0)
1057        .arg(&d_c1)
1058        .arg(&d_c2)
1059        .arg(&d_c3)
1060        .arg(&d_a)
1061        .arg(&d_aa)
1062        .arg(&d_r)
1063        .arg(&d_ar)
1064        .arg(&d_sbb)
1065        .arg(&d_sbh)
1066        .arg(&d_sbw)
1067        .arg(d_moments_ref)
1068        .arg(&d_chi)
1069        .arg(&d_xi)
1070        .arg(&d_rho)
1071        .arg(&d_tau)
1072        .arg(&d_ruv)
1073        .arg(&d_e_obs)
1074        .arg(&mut d_f_au)
1075        .arg(&mut d_neglog)
1076        .arg(&mut d_grad)
1077        .arg(&mut d_hess)
1078        .arg(&mut d_status);
1079
1080    // SAFETY: every kernel parameter above is either a primitive `i32` /
1081    // `f64` (passed by value), a const device pointer to a buffer whose
1082    // length the host validated against the input struct, or an output
1083    // buffers pre-allocated to checked `n_rows`, `n_rows*r`, and
1084    // `n_rows*r*r` lengths. Primary-width scratch aliases those outputs plus
1085    // the checked `d_f_au` allocation; no fixed-width device array exists.
1086    unsafe { builder.launch(cfg) }.map_err(|err| GpuError::DriverCallFailed {
1087        reason: format!("bms_flex_row launch: {err}"),
1088    })?;
1089    stream
1090        .synchronize()
1091        .map_err(|err| GpuError::DriverCallFailed {
1092            reason: format!("bms_flex_row synchronize: {err}"),
1093        })?;
1094
1095    let status = stream
1096        .clone_dtoh(&d_status)
1097        .map_err(|err| GpuError::DriverCallFailed {
1098            reason: format!("bms_flex_row download status: {err}"),
1099        })?;
1100    if let Some((row, code)) = status
1101        .iter()
1102        .copied()
1103        .enumerate()
1104        .find(|(_, code)| *code != 0)
1105    {
1106        return Err(GpuError::DriverCallFailed {
1107            reason: format!("bms_flex_row rejected non-finite row {row} with status {code}"),
1108        });
1109    }
1110
1111    let neglog = stream
1112        .clone_dtoh(&d_neglog)
1113        .map_err(|err| GpuError::DriverCallFailed {
1114            reason: format!("bms_flex_row download neglog: {err}"),
1115        })?;
1116    let grad = stream
1117        .clone_dtoh(&d_grad)
1118        .map_err(|err| GpuError::DriverCallFailed {
1119            reason: format!("bms_flex_row download grad: {err}"),
1120        })?;
1121    let hess = stream
1122        .clone_dtoh(&d_hess)
1123        .map_err(|err| GpuError::DriverCallFailed {
1124            reason: format!("bms_flex_row download hess: {err}"),
1125        })?;
1126
1127    Ok(BmsFlexRowKernelOutputs { neglog, grad, hess })
1128}
1129
1130// ─────────────────────────────────────────────────────────────────────────────
1131// Phase 3: device-resident row Hessian + HVP / diagonal kernels.
1132//
1133// Math (mirrors the CPU oracle in
1134// `src/families/bernoulli_marginal_slope.rs::exact_newton_joint_hessian_*_from_cache`):
1135//
1136//   Block layout (joint β):
1137//     marginal = [0..p_m), logslope = [p_m..p_m+p_g),
1138//     h        = [h_start..h_end), w = [w_start..w_end), total = p_total.
1139//
1140//   Primary layout (per-row r-vector):
1141//     q = 0, logslope = 1,
1142//     h = [h_primary_start..h_primary_end),
1143//     w = [w_primary_start..w_primary_end), total = r.
1144//
1145//   row_dir[u] for u in primary layout:
1146//     row_dir[0]   = Σ_j marginal_design[row, j] · v[j]
1147//     row_dir[1]   = Σ_j logslope_design[row, j] · v[p_m + j]
1148//     row_dir[h_k] = v[h_block_start + (h_k - h_primary_start)]
1149//     row_dir[w_k] = v[w_block_start + (w_k - w_primary_start)]
1150//
1151//   action[u]    = Σ_v row_hessians[row, u*r + v] · row_dir[v]
1152//
1153//   block_partial[marginal_j] += action[0] · marginal_design[row, j]
1154//   block_partial[logslope_j] += action[1] · logslope_design[row, j]
1155//   block_partial[h_block_start + (h_k - h_primary_start)] += action[h_k]
1156//   block_partial[w_block_start + (w_k - w_primary_start)] += action[w_k]
1157//
1158// Diagonal:
1159//   diag[marginal_j] += row_hess[row, 0*r + 0] · marginal_design[row, j]²
1160//   diag[logslope_j] += row_hess[row, 1*r + 1] · logslope_design[row, j]²
1161//   diag[h_block_start + k] += row_hess[row, ii*r + ii]   (ii = h_primary_start + k)
1162//   diag[w_block_start + k] += row_hess[row, ii*r + ii]   (ii = w_primary_start + k)
1163//
1164// Determinism: each CTA owns a contiguous slice of `[chunk_start..chunk_end)`
1165// rows and writes its full per-chunk `p_total` partial into a non-overlapping
1166// region of the global partial buffer. The reduce kernel then sums those
1167// partials in fixed chunk-major order. No atomics.
1168
1169/// Joint-β block layout shared with the host (mirrors `BlockSlices` in
1170/// `bernoulli_marginal_slope.rs`).
1171///
1172/// Gating: Linux-only. The lone production constructor lives in
1173/// `bernoulli_marginal_slope.rs:9189` behind `#[cfg(target_os = "linux")]`
1174/// — the device-resident row-Hessian path is the only producer (see
1175/// `launch_bms_flex_row_kernel_device_resident`), and the joint-β
1176/// consumers `launch_bms_flex_row_hvp` / `_diagonal` / `_dense_block`
1177/// are also Linux-only. Any non-Linux test referencing this type must
1178/// guard itself with `#[cfg(target_os = "linux")]` too — the build.rs
1179/// ban scanner explicitly rejects `#[cfg(any(..., test))]` on items as
1180/// a dead-code escape hatch.
1181#[cfg(target_os = "linux")]
1182#[derive(Clone, Debug)]
1183pub(crate) struct BmsFlexBlockLayout {
1184    pub p_m: usize,
1185    pub p_g: usize,
1186    pub h: Option<std::ops::Range<usize>>,
1187    pub w: Option<std::ops::Range<usize>>,
1188    pub p_total: usize,
1189}
1190
1191/// Primary-r layout shared with the host (mirrors `PrimarySlices`).
1192/// Gating rationale identical to [`BmsFlexBlockLayout`].
1193#[cfg(target_os = "linux")]
1194#[derive(Clone, Debug)]
1195pub(crate) struct BmsFlexPrimaryLayout {
1196    pub h: Option<std::ops::Range<usize>>,
1197    pub w: Option<std::ops::Range<usize>>,
1198    pub r: usize,
1199}
1200
1201// ── Linux-only: device-resident row-Hessian state + kernels ─────────────────
1202
1203/// Number of rows each HVP / diagonal CTA processes. Each CTA writes a single
1204/// `[1, p_total]` partial row into the global partial buffer (no atomics);
1205/// the reduce kernel then sums partials in chunk-major fixed order.
1206#[cfg(target_os = "linux")]
1207pub(crate) const HVP_ROWS_PER_CTA: u32 = 256;
1208
1209/// `blockDim.x` for the HVP / diagonal partial kernels.
1210#[cfg(target_os = "linux")]
1211pub(crate) const HVP_THREADS: u32 = 128;
1212
1213/// `blockDim.x` for the partial-sum reduction kernels (one element per thread,
1214/// grid-strided over the `p_total`/`rhs_elems` partial buffer). A full warp
1215/// multiple that keeps the reduce launch occupancy-bound rather than tail-bound
1216/// for the typical large-scale `p_total`.
1217#[cfg(target_os = "linux")]
1218pub(crate) const REDUCTION_THREADS: u32 = 256;
1219
1220/// Maximum RHS columns fused into one row-primary HVP launch. This is an
1221/// internal batching width, not a matrix-width limit: wider dense matrices are
1222/// materialised in consecutive batches. The CUDA source has four scalar
1223/// shared arrays of this length; primary directions are derived on demand.
1224#[cfg(target_os = "linux")]
1225pub(crate) const BMS_FLEX_ROW_HVP_MAX_RHS: usize = 8;
1226
1227/// Device-resident state produced by
1228/// `launch_bms_flex_row_kernel_device_resident` and consumed by
1229/// `launch_bms_flex_row_hvp` / `launch_bms_flex_row_diagonal`.
1230///
1231/// Owns the canonical row value, gradient, Hessian, and design slices on-device
1232/// so every downstream value/score/Hessian consumer shares one row evaluation
1233/// without round-tripping the large cache through host RAM. Drop releases the
1234/// device memory back to the CUDA runtime.
1235#[cfg(target_os = "linux")]
1236pub struct DeviceResidentRowHess {
1237    /// Per-row negative log likelihood emitted by the same canonical row
1238    /// program that emits `grad` and `hess`.
1239    pub(crate) neglog: CudaSlice<f64>,
1240    /// Per-row objective gradient `[n, r]`, row-major. Joint-score consumers
1241    /// negate and pull this buffer back through the two designs/direct blocks.
1242    pub(crate) grad: CudaSlice<f64>,
1243    /// Per-row dense `[n, r, r]` row-major Hessian. Element `(u, v)` of row
1244    /// `i` is `hess[i*r*r + u*r + v]`. This is the only on-device storage
1245    /// layout supported by the current HVP / diag kernels.
1246    pub(crate) hess: CudaSlice<f64>,
1247    pub(crate) marginal_design: CudaSlice<f64>,
1248    pub(crate) logslope_design: CudaSlice<f64>,
1249    pub(crate) n: usize,
1250    pub(crate) r: usize,
1251    pub(crate) block: BmsFlexBlockLayout,
1252    pub(crate) primary: BmsFlexPrimaryLayout,
1253    /// Estimated bytes resident on device (for accounting).
1254    pub(crate) bytes: u64,
1255}
1256
1257/// Host image of the deterministic device reduction over the canonical row
1258/// value/gradient buffers. The gradient uses log-likelihood/score sign.
1259#[cfg(target_os = "linux")]
1260pub(crate) struct BmsFlexDeviceJointGradient {
1261    pub(crate) log_likelihood: f64,
1262    pub(crate) gradient: Vec<f64>,
1263}
1264
1265#[cfg(target_os = "linux")]
1266impl std::fmt::Debug for DeviceResidentRowHess {
1267    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1268        f.debug_struct("DeviceResidentRowHess")
1269            .field("n", &self.n)
1270            .field("r", &self.r)
1271            .field("p_total", &self.block.p_total)
1272            .field("bytes", &self.bytes)
1273            .finish()
1274    }
1275}
1276
1277/// Sized-to-fit-once CTA mapping. Rows `[c * HVP_ROWS_PER_CTA, (c+1) * HVP_ROWS_PER_CTA)`
1278/// belong to chunk `c`.
1279#[cfg(target_os = "linux")]
1280pub(crate) fn num_hvp_chunks(n: usize) -> usize {
1281    n.div_ceil(HVP_ROWS_PER_CTA as usize)
1282}
1283
1284/// NVRTC source: deterministic joint-gradient, HVP, diagonal, and dense
1285/// partial+reduce kernels. All kernels mirror CPU oracles in this file.
1286#[cfg(target_os = "linux")]
1287pub(crate) const HVP_KERNEL_SOURCE: &str = r#"
1288// CPU parity reference: cpu_oracle_bms_flex_row_hvp / cpu_oracle_bms_flex_row_diagonal
1289// in this module.
1290
1291#define MAX_MULTI_RHS 8
1292
1293__device__ __forceinline__ double bms_flex_primary_direction(
1294    int primary_idx,
1295    int h_block_start,
1296    int h_block_len,
1297    int w_block_start,
1298    int w_block_len,
1299    int h_primary_start,
1300    int w_primary_start,
1301    double direction_q,
1302    double direction_g,
1303    const double * __restrict__ v)
1304{
1305    if (primary_idx == 0) return direction_q;
1306    if (primary_idx == 1) return direction_g;
1307    if (primary_idx >= h_primary_start && primary_idx < h_primary_start + h_block_len) {
1308        return v[h_block_start + primary_idx - h_primary_start];
1309    }
1310    if (primary_idx >= w_primary_start && primary_idx < w_primary_start + w_block_len) {
1311        return v[w_block_start + primary_idx - w_primary_start];
1312    }
1313    return 0.0;
1314}
1315
1316extern "C" __global__ void bms_flex_row_hvp_partial(
1317    int                  n_rows,
1318    int                  r,
1319    int                  p_m,
1320    int                  p_g,
1321    int                  p_total,
1322    int                  h_block_start,
1323    int                  h_block_len,
1324    int                  w_block_start,
1325    int                  w_block_len,
1326    int                  h_primary_start,
1327    int                  w_primary_start,
1328    int                  rows_per_cta,
1329    const double * __restrict__ row_hessians,    // [n, r*r]
1330    const double * __restrict__ marginal_design, // [n, p_m] row-major
1331    const double * __restrict__ logslope_design, // [n, p_g] row-major
1332    const double * __restrict__ v,               // [p_total]
1333    double       * __restrict__ partial)         // [num_chunks, p_total]
1334{
1335    int chunk = blockIdx.x;
1336    int tid   = threadIdx.x;
1337    int row_lo = chunk * rows_per_cta;
1338    int remaining_rows = n_rows - row_lo;
1339    int row_hi = row_lo + (remaining_rows < rows_per_cta ? remaining_rows : rows_per_cta);
1340
1341    // Zero this chunk's partial slice cooperatively.
1342    double *out = partial + (size_t)chunk * (size_t)p_total;
1343    for (int j = tid; j < p_total; j += blockDim.x) {
1344        out[j] = 0.0;
1345    }
1346    __syncthreads();
1347
1348    // Width-general scratch: only the two design directions/actions are
1349    // shared. Every h/w direction is read directly from v, and the thread
1350    // owning primary coordinate u accumulates that coordinate's action.
1351    __shared__ double direction_q;
1352    __shared__ double direction_g;
1353    __shared__ double action_q;
1354    __shared__ double action_g;
1355    __shared__ double dot_reduce[128];
1356
1357    for (int row = row_lo; row < row_hi; ++row) {
1358        const double *mrow = marginal_design + (size_t)row * (size_t)p_m;
1359        const double *grow = logslope_design + (size_t)row * (size_t)p_g;
1360        const double *Hrow = row_hessians + (size_t)row * (size_t)r * (size_t)r;
1361
1362        // row_dir[0] = mrow · v[0..p_m]
1363        double local = 0.0;
1364        for (int j = tid; j < p_m; j += blockDim.x) {
1365            local += mrow[j] * v[j];
1366        }
1367        dot_reduce[tid] = local;
1368        __syncthreads();
1369        for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
1370            if (tid < stride) dot_reduce[tid] += dot_reduce[tid + stride];
1371            __syncthreads();
1372        }
1373        if (tid == 0) direction_q = dot_reduce[0];
1374
1375        // row_dir[1] = grow · v[p_m..p_m+p_g]
1376        local = 0.0;
1377        for (int j = tid; j < p_g; j += blockDim.x) {
1378            local += grow[j] * v[p_m + j];
1379        }
1380        dot_reduce[tid] = local;
1381        __syncthreads();
1382        for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
1383            if (tid < stride) dot_reduce[tid] += dot_reduce[tid + stride];
1384            __syncthreads();
1385        }
1386        if (tid == 0) direction_g = dot_reduce[0];
1387        __syncthreads();
1388
1389        for (int u = tid; u < r; u += blockDim.x) {
1390            double acc = 0.0;
1391            for (int vv = 0; vv < r; ++vv) {
1392                double row_direction = bms_flex_primary_direction(
1393                    vv,
1394                    h_block_start, h_block_len,
1395                    w_block_start, w_block_len,
1396                    h_primary_start, w_primary_start,
1397                    direction_q, direction_g, v);
1398                acc += Hrow[(size_t)u * (size_t)r + (size_t)vv] * row_direction;
1399            }
1400            if (u == 0) {
1401                action_q = acc;
1402            } else if (u == 1) {
1403                action_g = acc;
1404            } else if (u >= h_primary_start && u < h_primary_start + h_block_len) {
1405                out[h_block_start + u - h_primary_start] += acc;
1406            } else if (u >= w_primary_start && u < w_primary_start + w_block_len) {
1407                out[w_block_start + u - w_primary_start] += acc;
1408            }
1409        }
1410        __syncthreads();
1411
1412        // Pull back into joint β slot.
1413        double a0 = action_q;
1414        for (int j = tid; j < p_m; j += blockDim.x) {
1415            out[j] += a0 * mrow[j];
1416        }
1417        double a1 = action_g;
1418        for (int j = tid; j < p_g; j += blockDim.x) {
1419            out[p_m + j] += a1 * grow[j];
1420        }
1421        __syncthreads();
1422    }
1423}
1424
1425extern "C" __global__ void bms_flex_row_hvp_reduce(
1426    int                  num_chunks,
1427    int                  p_total,
1428    const double * __restrict__ partial,   // [num_chunks, p_total]
1429    double       * __restrict__ out)        // [p_total]
1430{
1431    int j = blockIdx.x * blockDim.x + threadIdx.x;
1432    if (j >= p_total) return;
1433    double acc = 0.0;
1434    for (int c = 0; c < num_chunks; ++c) {
1435        acc += partial[(size_t)c * (size_t)p_total + (size_t)j];
1436    }
1437    out[j] = acc;
1438}
1439
1440extern "C" __global__ void bms_flex_row_joint_gradient_partial(
1441    int                  n_rows,
1442    int                  r,
1443    int                  p_m,
1444    int                  p_g,
1445    int                  p_total,
1446    int                  h_block_start,
1447    int                  h_block_len,
1448    int                  w_block_start,
1449    int                  w_block_len,
1450    int                  h_primary_start,
1451    int                  w_primary_start,
1452    int                  rows_per_cta,
1453    const double * __restrict__ row_neglog,       // [n]
1454    const double * __restrict__ row_grad,         // [n, r]
1455    const double * __restrict__ marginal_design,  // [n, p_m]
1456    const double * __restrict__ logslope_design,  // [n, p_g]
1457    double       * __restrict__ partial)          // [num_chunks, 1+p_total]
1458{
1459    int chunk = blockIdx.x;
1460    int tid = threadIdx.x;
1461    int row_lo = chunk * rows_per_cta;
1462    int remaining_rows = n_rows - row_lo;
1463    int row_hi = row_lo + (remaining_rows < rows_per_cta ? remaining_rows : rows_per_cta);
1464    int output_width = p_total + 1;
1465    double *out = partial + (size_t)chunk * (size_t)output_width;
1466
1467    // One thread owns each output coordinate for the whole row chunk. The
1468    // inner row loop therefore has a fixed order and needs no atomics.
1469    for (int output_idx = tid; output_idx < output_width; output_idx += blockDim.x) {
1470        double acc = 0.0;
1471        if (output_idx == 0) {
1472            for (int row = row_lo; row < row_hi; ++row) {
1473                acc -= row_neglog[row];
1474            }
1475        } else {
1476            int beta_idx = output_idx - 1;
1477            if (beta_idx < p_m) {
1478                for (int row = row_lo; row < row_hi; ++row) {
1479                    acc -= row_grad[(size_t)row * (size_t)r]
1480                         * marginal_design[(size_t)row * (size_t)p_m + (size_t)beta_idx];
1481                }
1482            } else if (beta_idx < p_m + p_g) {
1483                int j = beta_idx - p_m;
1484                for (int row = row_lo; row < row_hi; ++row) {
1485                    acc -= row_grad[(size_t)row * (size_t)r + 1]
1486                         * logslope_design[(size_t)row * (size_t)p_g + (size_t)j];
1487                }
1488            } else if (beta_idx >= h_block_start && beta_idx < h_block_start + h_block_len) {
1489                int primary_idx = h_primary_start + beta_idx - h_block_start;
1490                for (int row = row_lo; row < row_hi; ++row) {
1491                    acc -= row_grad[(size_t)row * (size_t)r + (size_t)primary_idx];
1492                }
1493            } else if (beta_idx >= w_block_start && beta_idx < w_block_start + w_block_len) {
1494                int primary_idx = w_primary_start + beta_idx - w_block_start;
1495                for (int row = row_lo; row < row_hi; ++row) {
1496                    acc -= row_grad[(size_t)row * (size_t)r + (size_t)primary_idx];
1497                }
1498            }
1499        }
1500        out[output_idx] = acc;
1501    }
1502}
1503
1504extern "C" __global__ void bms_flex_row_joint_gradient_reduce(
1505    int                  num_chunks,
1506    int                  output_width,
1507    const double * __restrict__ partial,   // [num_chunks, output_width]
1508    double       * __restrict__ out)        // [output_width]
1509{
1510    int j = blockIdx.x * blockDim.x + threadIdx.x;
1511    if (j >= output_width) return;
1512    double acc = 0.0;
1513    for (int c = 0; c < num_chunks; ++c) {
1514        acc += partial[(size_t)c * (size_t)output_width + (size_t)j];
1515    }
1516    out[j] = acc;
1517}
1518
1519extern "C" __global__ void bms_flex_row_hvp_multi_partial(
1520    int                  n_rows,
1521    int                  r,
1522    int                  p_m,
1523    int                  p_g,
1524    int                  p_total,
1525    int                  h_block_start,
1526    int                  h_block_len,
1527    int                  w_block_start,
1528    int                  w_block_len,
1529    int                  h_primary_start,
1530    int                  w_primary_start,
1531    int                  rows_per_cta,
1532    int                  rhs_count,
1533    const double * __restrict__ row_hessians,    // [n, r*r]
1534    const double * __restrict__ marginal_design, // [n, p_m]
1535    const double * __restrict__ logslope_design, // [n, p_g]
1536    const double * __restrict__ v_rhs,           // [rhs_count, p_total]
1537    double       * __restrict__ partial)         // [rhs_count, num_chunks, p_total]
1538{
1539    int chunk = blockIdx.x;
1540    int tid   = threadIdx.x;
1541    int row_lo = chunk * rows_per_cta;
1542    int remaining_rows = n_rows - row_lo;
1543    int row_hi = row_lo + (remaining_rows < rows_per_cta ? remaining_rows : rows_per_cta);
1544
1545    int num_chunks = 1 + (n_rows - 1) / rows_per_cta;
1546    for (int idx = tid; idx < rhs_count * p_total; idx += blockDim.x) {
1547        int rhs = idx / p_total;
1548        int j = idx - rhs * p_total;
1549        partial[((size_t)rhs * (size_t)num_chunks + (size_t)chunk) * (size_t)p_total + (size_t)j] = 0.0;
1550    }
1551    __syncthreads();
1552
1553    __shared__ double direction_q[MAX_MULTI_RHS];
1554    __shared__ double direction_g[MAX_MULTI_RHS];
1555    __shared__ double action_q[MAX_MULTI_RHS];
1556    __shared__ double action_g[MAX_MULTI_RHS];
1557    __shared__ double dot_reduce[128];
1558
1559    for (int row = row_lo; row < row_hi; ++row) {
1560        const double *mrow = marginal_design + (size_t)row * (size_t)p_m;
1561        const double *grow = logslope_design + (size_t)row * (size_t)p_g;
1562        const double *Hrow = row_hessians + (size_t)row * (size_t)r * (size_t)r;
1563
1564        for (int rhs = 0; rhs < rhs_count; ++rhs) {
1565            const double *v = v_rhs + (size_t)rhs * (size_t)p_total;
1566
1567            double local = 0.0;
1568            for (int j = tid; j < p_m; j += blockDim.x) {
1569                local += mrow[j] * v[j];
1570            }
1571            dot_reduce[tid] = local;
1572            __syncthreads();
1573            for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
1574                if (tid < stride) dot_reduce[tid] += dot_reduce[tid + stride];
1575                __syncthreads();
1576            }
1577            if (tid == 0) direction_q[rhs] = dot_reduce[0];
1578
1579            local = 0.0;
1580            for (int j = tid; j < p_g; j += blockDim.x) {
1581                local += grow[j] * v[p_m + j];
1582            }
1583            dot_reduce[tid] = local;
1584            __syncthreads();
1585            for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
1586                if (tid < stride) dot_reduce[tid] += dot_reduce[tid + stride];
1587                __syncthreads();
1588            }
1589            if (tid == 0) direction_g[rhs] = dot_reduce[0];
1590            __syncthreads();
1591        }
1592
1593        size_t total_actions = (size_t)rhs_count * (size_t)r;
1594        for (size_t idx = (size_t)tid; idx < total_actions; idx += (size_t)blockDim.x) {
1595            int rhs = (int)(idx / (size_t)r);
1596            int u = (int)(idx - (size_t)rhs * (size_t)r);
1597            const double *v = v_rhs + (size_t)rhs * (size_t)p_total;
1598            double *out = partial + ((size_t)rhs * (size_t)num_chunks + (size_t)chunk) * (size_t)p_total;
1599            double acc = 0.0;
1600            for (int vv = 0; vv < r; ++vv) {
1601                double row_direction = bms_flex_primary_direction(
1602                    vv,
1603                    h_block_start, h_block_len,
1604                    w_block_start, w_block_len,
1605                    h_primary_start, w_primary_start,
1606                    direction_q[rhs], direction_g[rhs], v);
1607                acc += Hrow[(size_t)u * (size_t)r + (size_t)vv] * row_direction;
1608            }
1609            if (u == 0) {
1610                action_q[rhs] = acc;
1611            } else if (u == 1) {
1612                action_g[rhs] = acc;
1613            } else if (u >= h_primary_start && u < h_primary_start + h_block_len) {
1614                out[h_block_start + u - h_primary_start] += acc;
1615            } else if (u >= w_primary_start && u < w_primary_start + w_block_len) {
1616                out[w_block_start + u - w_primary_start] += acc;
1617            }
1618        }
1619        __syncthreads();
1620
1621        for (int rhs = 0; rhs < rhs_count; ++rhs) {
1622            double *out = partial + ((size_t)rhs * (size_t)num_chunks + (size_t)chunk) * (size_t)p_total;
1623            double a0 = action_q[rhs];
1624            for (int j = tid; j < p_m; j += blockDim.x) {
1625                out[j] += a0 * mrow[j];
1626            }
1627            double a1 = action_g[rhs];
1628            for (int j = tid; j < p_g; j += blockDim.x) {
1629                out[p_m + j] += a1 * grow[j];
1630            }
1631            __syncthreads();
1632        }
1633    }
1634}
1635
1636extern "C" __global__ void bms_flex_row_hvp_multi_reduce(
1637    int                  num_chunks,
1638    int                  p_total,
1639    int                  rhs_count,
1640    const double * __restrict__ partial,   // [rhs_count, num_chunks, p_total]
1641    double       * __restrict__ out)        // [rhs_count, p_total]
1642{
1643    int idx = blockIdx.x * blockDim.x + threadIdx.x;
1644    int total = rhs_count * p_total;
1645    if (idx >= total) return;
1646    int rhs = idx / p_total;
1647    int j = idx - rhs * p_total;
1648    double acc = 0.0;
1649    for (int c = 0; c < num_chunks; ++c) {
1650        acc += partial[((size_t)rhs * (size_t)num_chunks + (size_t)c) * (size_t)p_total + (size_t)j];
1651    }
1652    out[(size_t)rhs * (size_t)p_total + (size_t)j] = acc;
1653}
1654
1655extern "C" __global__ void bms_flex_row_diag_partial(
1656    int                  n_rows,
1657    int                  r,
1658    int                  p_m,
1659    int                  p_g,
1660    int                  p_total,
1661    int                  h_block_start,
1662    int                  h_block_len,
1663    int                  w_block_start,
1664    int                  w_block_len,
1665    int                  h_primary_start,
1666    int                  w_primary_start,
1667    int                  rows_per_cta,
1668    const double * __restrict__ row_hessians,
1669    const double * __restrict__ marginal_design,
1670    const double * __restrict__ logslope_design,
1671    double       * __restrict__ partial)
1672{
1673    int chunk = blockIdx.x;
1674    int tid   = threadIdx.x;
1675    int row_lo = chunk * rows_per_cta;
1676    int remaining_rows = n_rows - row_lo;
1677    int row_hi = row_lo + (remaining_rows < rows_per_cta ? remaining_rows : rows_per_cta);
1678
1679    double *out = partial + (size_t)chunk * (size_t)p_total;
1680    for (int j = tid; j < p_total; j += blockDim.x) {
1681        out[j] = 0.0;
1682    }
1683    __syncthreads();
1684
1685    for (int row = row_lo; row < row_hi; ++row) {
1686        const double *mrow = marginal_design + (size_t)row * (size_t)p_m;
1687        const double *grow = logslope_design + (size_t)row * (size_t)p_g;
1688        const double *Hrow = row_hessians + (size_t)row * (size_t)r * (size_t)r;
1689        double h00 = Hrow[0];
1690        double h11 = Hrow[(size_t)r + 1U];
1691        for (int j = tid; j < p_m; j += blockDim.x) {
1692            double v = mrow[j];
1693            out[j] += h00 * v * v;
1694        }
1695        for (int j = tid; j < p_g; j += blockDim.x) {
1696            double v = grow[j];
1697            out[p_m + j] += h11 * v * v;
1698        }
1699        if (tid == 0) {
1700            for (int k = 0; k < h_block_len; ++k) {
1701                int ii = h_primary_start + k;
1702                out[h_block_start + k] +=
1703                    Hrow[(size_t)ii * (size_t)r + (size_t)ii];
1704            }
1705            for (int k = 0; k < w_block_len; ++k) {
1706                int ii = w_primary_start + k;
1707                out[w_block_start + k] +=
1708                    Hrow[(size_t)ii * (size_t)r + (size_t)ii];
1709            }
1710        }
1711        __syncthreads();
1712    }
1713}
1714
1715// ────────────────────────────────────────────────────────────────────────
1716// Phase 6 — dense joint-Hessian block kernel for the debug / exact-REML
1717// route. Materialises the full `[p_total, p_total]` row-major joint H
1718// from the per-row r×r Hessian via the P_i pullback. NOT the default
1719// Newton path: production Newton uses HVP (Phase 2/3); this kernel exists
1720// for exact-REML logdet / dense-H comparisons / diagnostic dumps where the
1721// caller genuinely needs the dense matrix on the device.
1722//
1723// Per-CTA partial: each CTA owns a contiguous chunk of rows
1724// `[chunk*rows_per_cta, (chunk+1)*rows_per_cta)`. Inside the CTA the
1725// per-row pullback computes `(P_i^T H_i P_i)[m, n]` and adds it to the
1726// CTA's shared-mem `[p_total, p_total]` partial. The reduce kernel sums
1727// chunk-major-fixed-order into a single `[p_total, p_total]` output.
1728//
1729// Math: for primary index u ∈ [0, r):
1730//   * u = 0:        phi_u = (X_i in slot 0..p_m, 0 elsewhere)
1731//   * u = 1:        phi_u = (0, G_i in slot p_m..p_m+p_g, 0 elsewhere)
1732//   * u = 2+j:      phi_u = e_{h_block_start + j}  (j ∈ 0..h_block_len)
1733//   * u = 2+h+l:    phi_u = e_{w_block_start + l}  (l ∈ 0..w_block_len)
1734// Then `H_full[m, n] += sum_{u,v} H_i[u,v] * phi_u[m] * phi_v[n]`.
1735//
1736// Shared-memory budget: at large-scale shape p_total = 44, a [44, 44] f64
1737// partial is 44*44*8 = 15.5 KiB — well below the V100 48 KiB/SM cap.
1738// At p_total ≤ 80 the kernel still fits (80*80*8 = 50 KiB → just over
1739// V100 cap; caller must enforce p_total ≤ DENSE_BLOCK_MAX_P). The
1740// launcher rejects oversize p_total cleanly.
1741
1742extern "C" __global__ void bms_flex_row_dense_block_partial(
1743    int                  n_rows,
1744    int                  r,
1745    int                  p_m,
1746    int                  p_g,
1747    int                  p_total,
1748    int                  h_block_start,
1749    int                  h_block_len,
1750    int                  w_block_start,
1751    int                  w_block_len,
1752    int                  h_primary_start,
1753    int                  w_primary_start,
1754    int                  rows_per_cta,
1755    const double * __restrict__ row_hessians,    // [n, r*r]
1756    const double * __restrict__ marginal_design, // [n, p_m]
1757    const double * __restrict__ logslope_design, // [n, p_g]
1758    double       * __restrict__ partial)         // [num_chunks, p_total, p_total]
1759{
1760    extern __shared__ double shmem[];
1761    int chunk = blockIdx.x;
1762    int tid   = threadIdx.x;
1763    int row_lo = chunk * rows_per_cta;
1764    int remaining_rows = n_rows - row_lo;
1765    int row_hi = row_lo + (remaining_rows < rows_per_cta ? remaining_rows : rows_per_cta);
1766
1767    int pp = p_total * p_total;
1768    double *acc = shmem; // CTA-private accumulator [p_total, p_total]
1769    for (int j = tid; j < pp; j += blockDim.x) acc[j] = 0.0;
1770    __syncthreads();
1771
1772    // Per-row work performed by thread 0 to avoid cross-thread RW
1773    // contention on `acc[]`. Per-row complexity is O(r² + p_total²); the host
1774    // selects this direct algorithm only for small p_total, while r remains a
1775    // checked runtime width with no semantic ceiling.
1776    // Tighter parallel implementations are possible (warp-stripe the
1777    // 4-way nested u-v-m-n loop) but Phase 6 is a debug-only path and
1778    // the simple version is easier to audit for correctness against
1779    // the host-side P_i pullback oracle.
1780    if (tid == 0) {
1781        for (int row = row_lo; row < row_hi; ++row) {
1782            const double *mrow = marginal_design + (size_t)row * (size_t)p_m;
1783            const double *grow = logslope_design + (size_t)row * (size_t)p_g;
1784            const double *Hrow = row_hessians + (size_t)row * (size_t)r * (size_t)r;
1785            for (int u = 0; u < r; ++u) {
1786                for (int v = 0; v < r; ++v) {
1787                    double huv = Hrow[(size_t)u * (size_t)r + (size_t)v];
1788                    if (huv == 0.0) continue;
1789                    // For each (u, v), iterate (m, n) over the non-zero
1790                    // outer-product support of phi_u and phi_v.
1791                    // Build a small (offset, len, src_ptr) descriptor for
1792                    // each operand block as we go.
1793                    int m_off, m_len; const double *m_src; bool m_indicator;
1794                    int n_off, n_len; const double *n_src; bool n_indicator;
1795                    if (u == 0)      { m_off = 0;   m_len = p_m; m_src = mrow; m_indicator = false; }
1796                    else if (u == 1) { m_off = p_m; m_len = p_g; m_src = grow; m_indicator = false; }
1797                    else if (u - 2 < h_block_len) {
1798                                       m_off = h_block_start + (u - 2);
1799                                       m_len = 1;   m_src = NULL; m_indicator = true;
1800                    } else {
1801                                       m_off = w_block_start + (u - 2 - h_block_len);
1802                                       m_len = 1;   m_src = NULL; m_indicator = true;
1803                    }
1804                    if (v == 0)      { n_off = 0;   n_len = p_m; n_src = mrow; n_indicator = false; }
1805                    else if (v == 1) { n_off = p_m; n_len = p_g; n_src = grow; n_indicator = false; }
1806                    else if (v - 2 < h_block_len) {
1807                                       n_off = h_block_start + (v - 2);
1808                                       n_len = 1;   n_src = NULL; n_indicator = true;
1809                    } else {
1810                                       n_off = w_block_start + (v - 2 - h_block_len);
1811                                       n_len = 1;   n_src = NULL; n_indicator = true;
1812                    }
1813                    // accumulate huv * phi_u[m] * phi_v[n] into acc[m, n]
1814                    for (int mi = 0; mi < m_len; ++mi) {
1815                        double pm = m_indicator ? 1.0 : m_src[mi];
1816                        if (pm == 0.0) continue;
1817                        double scaled = huv * pm;
1818                        int m_idx = m_off + mi;
1819                        for (int ni = 0; ni < n_len; ++ni) {
1820                            double pn = n_indicator ? 1.0 : n_src[ni];
1821                            int n_idx = n_off + ni;
1822                            acc[m_idx * p_total + n_idx] += scaled * pn;
1823                        }
1824                    }
1825                }
1826            }
1827        }
1828    }
1829    __syncthreads();
1830
1831    // Write CTA accumulator out to global memory at its chunk slot.
1832    double *out_chunk = partial + (size_t)chunk * (size_t)pp;
1833    for (int j = tid; j < pp; j += blockDim.x) {
1834        out_chunk[j] = acc[j];
1835    }
1836}
1837
1838extern "C" __global__ void bms_flex_row_dense_block_reduce(
1839    int                  num_chunks,
1840    int                  p_total,
1841    const double * __restrict__ partial,
1842    double       * __restrict__ out)
1843{
1844    int j = blockIdx.x * blockDim.x + threadIdx.x;
1845    int pp = p_total * p_total;
1846    if (j >= pp) return;
1847    double acc = 0.0;
1848    for (int c = 0; c < num_chunks; ++c) {
1849        acc += partial[(size_t)c * (size_t)pp + (size_t)j];
1850    }
1851    out[j] = acc;
1852}
1853
1854"#;
1855
1856#[cfg(target_os = "linux")]
1857pub(crate) struct HvpKernelBackend {
1858    pub(crate) stream: Arc<CudaStream>,
1859    pub(crate) module: Arc<CudaModule>,
1860}
1861
1862#[cfg(target_os = "linux")]
1863impl HvpKernelBackend {
1864    pub(crate) fn probe() -> Result<&'static Self, GpuError> {
1865        static BACKEND: OnceLock<Result<HvpKernelBackend, GpuError>> = OnceLock::new();
1866        BACKEND
1867            .get_or_init(|| {
1868                gam_gpu::backend_probe::probe_backend_with_compile("bms_flex_row hvp", |parts| {
1869                    // #1551: arch-aware compile (see launch_bms_flex_row_kernel) —
1870                    // pin `--gpu-architecture` to the device capability and supply
1871                    // the standard include paths via the shared NVRTC entry point.
1872                    let ptx = gam_gpu::device_cache::compile_ptx_arch(HVP_KERNEL_SOURCE).map_err(
1873                        |err| GpuError::DriverCallFailed {
1874                            reason: format!("bms_flex_row hvp NVRTC compile failed: {err}"),
1875                        },
1876                    )?;
1877                    let module =
1878                        parts
1879                            .ctx
1880                            .load_module(ptx)
1881                            .map_err(|err| GpuError::DriverCallFailed {
1882                                reason: format!("bms_flex_row hvp module load failed: {err}"),
1883                            })?;
1884                    Ok(HvpKernelBackend {
1885                        stream: parts.stream.clone(),
1886                        module,
1887                    })
1888                })
1889            })
1890            .as_ref()
1891            .map_err(GpuError::clone)
1892    }
1893}
1894
1895/// Build a device-resident row-Hessian cache by launching the row kernel and
1896/// keeping the resulting `n × r²` slice resident on the device. Also uploads
1897/// the dense marginal + logslope design matrices so subsequent HVPs do not
1898/// re-upload them at every direction.
1899///
1900/// `marginal_design_row_major` and `logslope_design_row_major` must be
1901/// row-major `[n, p_m]` and `[n, p_g]` contiguous slices.
1902///
1903/// #461 absorber (additive Stage-1 influence block): the orthogonalization is
1904/// realized as **A2 — the marginal design widened to `[M | Z̃_infl]`** (see
1905/// `src/families/bms/block_specs.rs::widen_marginal_dense_with_influence`), NOT
1906/// a dedicated 5th primary coordinate. The absorber `+Z̃_infl·γ` is plain
1907/// additive into the marginal index `α(x)`, so γ lives inside `β_m` of the
1908/// widened block and `p_m` already counts the `p₁` influence columns. The row
1909/// kernel reads the marginal index from `block_states[0].eta` (which carries
1910/// `Z̃_infl·γ`) and pulls back through this same widened `marginal_design`, so
1911/// the absorber rides the existing primary-coordinate `u = 0` chain with **no
1912/// kernel-source change**: η, gradient, and Hessian match the CPU kernel
1913/// bit-for-bit precisely because `marginal_design` and `β_m` are the matched
1914/// (design, coefficient) pair the CPU path uses. The validation below pins
1915/// `marginal_design.len() == n·p_m` (with `p_m` widened), so a stale narrow
1916/// design against a widened `block.p_m` is rejected cleanly rather than
1917/// silently computing the wrong η. The absorber is dropped at
1918/// predict, where the marginal design is rebuilt without the influence columns,
1919/// so the predict-time `p_m` is narrow and this path is correct there too.
1920#[cfg(target_os = "linux")]
1921pub(crate) fn launch_bms_flex_row_kernel_device_resident(
1922    inputs: BmsFlexRowKernelInputs<'_>,
1923    marginal_design_row_major: &[f64],
1924    logslope_design_row_major: &[f64],
1925    block: BmsFlexBlockLayout,
1926    primary: BmsFlexPrimaryLayout,
1927) -> Result<DeviceResidentRowHess, GpuError> {
1928    inputs.validate()?;
1929    if !s_f_diagnostic_finite(&inputs) {
1930        return Err(GpuError::DriverCallFailed {
1931            reason: format!(
1932                "bms_flex_row device-resident: s_f must be positive and finite, got {}",
1933                inputs.s_f
1934            ),
1935        });
1936    }
1937    let n = inputs.n_rows;
1938    let r = inputs.r;
1939    let nr = checked_shape_len("device-resident [n,r]", &[n, r])?;
1940    let nrr = checked_shape_len("device-resident [n,r,r]", &[n, r, r])?;
1941    let marginal_len = checked_shape_len("device-resident marginal design", &[n, block.p_m])?;
1942    let logslope_len = checked_shape_len("device-resident logslope design", &[n, block.p_g])?;
1943    if marginal_design_row_major.len() != marginal_len {
1944        return Err(GpuError::DriverCallFailed {
1945            reason: format!(
1946                "bms_flex_row device-resident: marginal_design len={} != n*p_m={}",
1947                marginal_design_row_major.len(),
1948                marginal_len
1949            ),
1950        });
1951    }
1952    if logslope_design_row_major.len() != logslope_len {
1953        return Err(GpuError::DriverCallFailed {
1954            reason: format!(
1955                "bms_flex_row device-resident: logslope_design len={} != n*p_g={}",
1956                logslope_design_row_major.len(),
1957                logslope_len
1958            ),
1959        });
1960    }
1961    if primary.r != r {
1962        return Err(GpuError::DriverCallFailed {
1963            reason: format!(
1964                "bms_flex_row device-resident: primary.r={} != inputs.r={}",
1965                primary.r, r
1966            ),
1967        });
1968    }
1969
1970    // Ensure the row kernel backend is compiled & loaded (this also compiles
1971    // the HVP backend on first use so the caller surfaces failures here).
1972    let backend = RowKernelBackend::probe()?;
1973    HvpKernelBackend::probe()?;
1974    let stream = backend.stream.clone();
1975
1976    let upload_f64 = |slice: &[f64], label: &str| {
1977        stream
1978            .clone_htod(slice)
1979            .map_err(|err| GpuError::DriverCallFailed {
1980                reason: format!("bms_flex_row device-resident upload {label}: {err}"),
1981            })
1982    };
1983    let upload_u32 = |slice: &[u32], label: &str| {
1984        stream
1985            .clone_htod(slice)
1986            .map_err(|err| GpuError::DriverCallFailed {
1987                reason: format!("bms_flex_row device-resident upload {label}: {err}"),
1988            })
1989    };
1990
1991    let d_q = upload_f64(inputs.q, "q")?;
1992    let d_b = upload_f64(inputs.b, "b")?;
1993    let d_mu1 = upload_f64(inputs.mu_1, "mu_1")?;
1994    let d_mu2 = upload_f64(inputs.mu_2, "mu_2")?;
1995    let d_zobs = upload_f64(inputs.z_obs, "z_obs")?;
1996    let d_y = upload_f64(inputs.y, "y")?;
1997    let d_w = upload_f64(inputs.w, "w")?;
1998    let d_offsets = upload_u32(inputs.cell_offsets, "cell_offsets")?;
1999    let d_c0 = upload_f64(inputs.cell_c0, "cell_c0")?;
2000    let d_c1 = upload_f64(inputs.cell_c1, "cell_c1")?;
2001    let d_c2 = upload_f64(inputs.cell_c2, "cell_c2")?;
2002    let d_c3 = upload_f64(inputs.cell_c3, "cell_c3")?;
2003    let d_a = upload_f64(inputs.cell_a, "cell_a")?;
2004    let d_aa = upload_f64(inputs.cell_aa, "cell_aa")?;
2005    let d_r = upload_f64(inputs.cell_r, "cell_r")?;
2006    let d_ar = upload_f64(inputs.cell_ar, "cell_ar")?;
2007    let d_sbb = upload_f64(inputs.cell_sbb, "cell_sbb")?;
2008    let d_sbh = upload_f64(inputs.cell_sbh, "cell_sbh")?;
2009    let d_sbw = upload_f64(inputs.cell_sbw, "cell_sbw")?;
2010    // Phase-4: optionally consume device-resident moments (no host upload).
2011    let owned_host_moments: CudaSlice<f64>;
2012    let d_moments_ref: &CudaSlice<f64> = match &inputs.cell_moments {
2013        CellMomentsSource::Host(slice) => {
2014            owned_host_moments = upload_f64(slice, "cell_moments")?;
2015            &owned_host_moments
2016        }
2017        CellMomentsSource::Device(d) => *d,
2018    };
2019    let d_chi = upload_f64(inputs.chi_obs, "chi_obs")?;
2020    let d_xi = upload_f64(inputs.xi_obs, "xi_obs")?;
2021    let d_rho = upload_f64(inputs.rho_u, "rho_u")?;
2022    let d_tau = upload_f64(inputs.tau_u, "tau_u")?;
2023    let d_ruv = upload_f64(inputs.r_uv, "r_uv")?;
2024    let d_e_obs = upload_f64(inputs.e_obs, "e_obs")?;
2025
2026    let d_marginal = upload_f64(marginal_design_row_major, "marginal_design")?;
2027    let d_logslope = upload_f64(logslope_design_row_major, "logslope_design")?;
2028
2029    let mut d_neglog = stream
2030        .alloc_zeros::<f64>(n)
2031        .map_err(|err| GpuError::DriverCallFailed {
2032            reason: format!("bms_flex_row device-resident alloc neglog: {err}"),
2033        })?;
2034    let mut d_grad = stream
2035        .alloc_zeros::<f64>(nr)
2036        .map_err(|err| GpuError::DriverCallFailed {
2037            reason: format!("bms_flex_row device-resident alloc grad: {err}"),
2038        })?;
2039    let mut d_hess = stream
2040        .alloc_zeros::<f64>(nrr)
2041        .map_err(|err| GpuError::DriverCallFailed {
2042            reason: format!("bms_flex_row device-resident alloc hess: {err}"),
2043        })?;
2044    let mut d_f_au = stream
2045        .alloc_zeros::<f64>(nr)
2046        .map_err(|err| GpuError::DriverCallFailed {
2047            reason: format!("bms_flex_row device-resident alloc F_au scratch: {err}"),
2048        })?;
2049    let mut d_status = stream
2050        .alloc_zeros::<u32>(n)
2051        .map_err(|err| GpuError::DriverCallFailed {
2052            reason: format!("bms_flex_row device-resident alloc status: {err}"),
2053        })?;
2054
2055    let func = backend
2056        .module
2057        .load_function("bms_flex_row_kernel")
2058        .map_err(|err| GpuError::DriverCallFailed {
2059            reason: format!("bms_flex_row device-resident load_function: {err}"),
2060        })?;
2061
2062    let n_u32 = u32::try_from(n).map_err(|_| GpuError::DriverCallFailed {
2063        reason: format!("bms_flex_row device-resident: n_rows={n} exceeds CUDA grid range"),
2064    })?;
2065    let cfg = LaunchConfig {
2066        grid_dim: (n_u32, 1, 1),
2067        block_dim: (ROW_KERNEL_THREADS, 1, 1),
2068        shared_mem_bytes: 0,
2069    };
2070    let n_i32 = i32::try_from(n).map_err(|_| GpuError::DriverCallFailed {
2071        reason: format!("bms_flex_row device-resident: n_rows={n} exceeds i32 range"),
2072    })?;
2073    let r_i32 = i32::try_from(r).map_err(|_| GpuError::DriverCallFailed {
2074        reason: format!("bms_flex_row device-resident: r={r} exceeds i32 range"),
2075    })?;
2076    let p_h_i32 = i32::try_from(inputs.p_h).map_err(|_| GpuError::DriverCallFailed {
2077        reason: format!(
2078            "bms_flex_row device-resident: p_h={} exceeds i32 range",
2079            inputs.p_h
2080        ),
2081    })?;
2082    let p_w_i32 = i32::try_from(inputs.p_w).map_err(|_| GpuError::DriverCallFailed {
2083        reason: format!(
2084            "bms_flex_row device-resident: p_w={} exceeds i32 range",
2085            inputs.p_w
2086        ),
2087    })?;
2088    let s_f_val = inputs.s_f;
2089
2090    let mut builder = stream.launch_builder(&func);
2091    builder
2092        .arg(&n_i32)
2093        .arg(&r_i32)
2094        .arg(&p_h_i32)
2095        .arg(&p_w_i32)
2096        .arg(&s_f_val)
2097        .arg(&d_q)
2098        .arg(&d_b)
2099        .arg(&d_mu1)
2100        .arg(&d_mu2)
2101        .arg(&d_zobs)
2102        .arg(&d_y)
2103        .arg(&d_w)
2104        .arg(&d_offsets)
2105        .arg(&d_c0)
2106        .arg(&d_c1)
2107        .arg(&d_c2)
2108        .arg(&d_c3)
2109        .arg(&d_a)
2110        .arg(&d_aa)
2111        .arg(&d_r)
2112        .arg(&d_ar)
2113        .arg(&d_sbb)
2114        .arg(&d_sbh)
2115        .arg(&d_sbw)
2116        .arg(d_moments_ref)
2117        .arg(&d_chi)
2118        .arg(&d_xi)
2119        .arg(&d_rho)
2120        .arg(&d_tau)
2121        .arg(&d_ruv)
2122        .arg(&d_e_obs)
2123        .arg(&mut d_f_au)
2124        .arg(&mut d_neglog)
2125        .arg(&mut d_grad)
2126        .arg(&mut d_hess)
2127        .arg(&mut d_status);
2128    // SAFETY: same shape contract as `launch_linux`: every kernel parameter is
2129    // either a primitive scalar by-value, a const device pointer whose
2130    // capacity was validated by `inputs.validate()`, or one of the three
2131    // output buffers we just allocated with the expected element count.
2132    unsafe { builder.launch(cfg) }.map_err(|err| GpuError::DriverCallFailed {
2133        reason: format!("bms_flex_row device-resident launch: {err}"),
2134    })?;
2135    stream
2136        .synchronize()
2137        .map_err(|err| GpuError::DriverCallFailed {
2138            reason: format!("bms_flex_row device-resident synchronize: {err}"),
2139        })?;
2140
2141    let status = stream
2142        .clone_dtoh(&d_status)
2143        .map_err(|err| GpuError::DriverCallFailed {
2144            reason: format!("bms_flex_row device-resident download status: {err}"),
2145        })?;
2146    if let Some((row, code)) = status
2147        .iter()
2148        .copied()
2149        .enumerate()
2150        .find(|(_, code)| *code != 0)
2151    {
2152        return Err(GpuError::DriverCallFailed {
2153            reason: format!(
2154                "bms_flex_row device-resident rejected non-finite row {row} with status {code}"
2155            ),
2156        });
2157    }
2158    drop(d_status);
2159    drop(d_f_au);
2160
2161    // Drop the per-cell uploads; keep the canonical row value, gradient,
2162    // Hessian, and both pullback designs in one device-resident authority.
2163    drop(d_q);
2164    drop(d_b);
2165    drop(d_mu1);
2166    drop(d_mu2);
2167    drop(d_zobs);
2168    drop(d_y);
2169    drop(d_w);
2170    drop(d_offsets);
2171    drop(d_c0);
2172    drop(d_c1);
2173    drop(d_c2);
2174    drop(d_c3);
2175    drop(d_a);
2176    drop(d_aa);
2177    drop(d_r);
2178    drop(d_ar);
2179    drop(d_sbb);
2180    drop(d_sbh);
2181    drop(d_sbw);
2182    // `owned_host_moments` (if any) and the borrowed `d_moments_ref` both
2183    // go out of scope at the end of the function; the device-resident
2184    // moments owned by the caller stay alive.
2185    drop(d_chi);
2186    drop(d_xi);
2187    drop(d_rho);
2188    drop(d_tau);
2189    drop(d_ruv);
2190
2191    let resident_elements = n
2192        .checked_add(nr)
2193        .and_then(|value| value.checked_add(nrr))
2194        .and_then(|value| value.checked_add(marginal_len))
2195        .and_then(|value| value.checked_add(logslope_len))
2196        .ok_or_else(|| GpuError::DriverCallFailed {
2197            reason: "bms_flex_row device-resident: resident element count overflow".to_string(),
2198        })?;
2199    let resident_bytes = resident_elements
2200        .checked_mul(std::mem::size_of::<f64>())
2201        .ok_or_else(|| GpuError::DriverCallFailed {
2202            reason: "bms_flex_row device-resident: resident byte count overflow".to_string(),
2203        })?;
2204    let bytes = u64::try_from(resident_bytes).map_err(|_| GpuError::DriverCallFailed {
2205        reason: format!(
2206            "bms_flex_row device-resident: resident bytes={resident_bytes} exceed u64 range"
2207        ),
2208    })?;
2209    Ok(DeviceResidentRowHess {
2210        neglog: d_neglog,
2211        grad: d_grad,
2212        hess: d_hess,
2213        marginal_design: d_marginal,
2214        logslope_design: d_logslope,
2215        n,
2216        r,
2217        block,
2218        primary,
2219        bytes,
2220    })
2221}
2222
2223/// Reduce the canonical per-row value and objective gradient into the joint
2224/// log-likelihood and score gradient without re-running row calculus on CPU.
2225/// Both stages use fixed row/chunk order and no atomics.
2226#[cfg(target_os = "linux")]
2227pub(crate) fn launch_bms_flex_row_joint_gradient(
2228    storage: &DeviceResidentRowHess,
2229) -> Result<BmsFlexDeviceJointGradient, GpuError> {
2230    let p_total = storage.block.p_total;
2231    let output_width = p_total
2232        .checked_add(1)
2233        .ok_or_else(|| GpuError::DriverCallFailed {
2234            reason: "bms_flex_row joint gradient: output width overflow".to_string(),
2235        })?;
2236    if storage.n == 0 {
2237        return Ok(BmsFlexDeviceJointGradient {
2238            log_likelihood: 0.0,
2239            gradient: vec![0.0; p_total],
2240        });
2241    }
2242
2243    let backend = HvpKernelBackend::probe()?;
2244    let stream = backend.stream.clone();
2245    let args = PreparedBmsFlexRowLaunchArgs::from_storage(storage)?;
2246    let partial_len = args
2247        .num_chunks
2248        .checked_mul(output_width)
2249        .ok_or_else(|| GpuError::DriverCallFailed {
2250            reason: format!(
2251                "bms_flex_row joint gradient: partial length overflow for chunks={} width={output_width}",
2252                args.num_chunks
2253            ),
2254        })?;
2255    let mut d_partial =
2256        stream
2257            .alloc_zeros::<f64>(partial_len)
2258            .map_err(|err| GpuError::DriverCallFailed {
2259                reason: format!("bms_flex_row joint gradient alloc partial: {err}"),
2260            })?;
2261    let mut d_out =
2262        stream
2263            .alloc_zeros::<f64>(output_width)
2264            .map_err(|err| GpuError::DriverCallFailed {
2265                reason: format!("bms_flex_row joint gradient alloc output: {err}"),
2266            })?;
2267    let partial_func = backend
2268        .module
2269        .load_function("bms_flex_row_joint_gradient_partial")
2270        .map_err(|err| GpuError::DriverCallFailed {
2271            reason: format!("bms_flex_row joint gradient load partial: {err}"),
2272        })?;
2273    let reduce_func = backend
2274        .module
2275        .load_function("bms_flex_row_joint_gradient_reduce")
2276        .map_err(|err| GpuError::DriverCallFailed {
2277            reason: format!("bms_flex_row joint gradient load reduce: {err}"),
2278        })?;
2279
2280    let num_chunks_u32 =
2281        u32::try_from(args.num_chunks).map_err(|_| GpuError::DriverCallFailed {
2282            reason: format!(
2283                "bms_flex_row joint gradient: num_chunks={} exceeds u32 range",
2284                args.num_chunks
2285            ),
2286        })?;
2287    let cfg_partial = LaunchConfig {
2288        grid_dim: (num_chunks_u32, 1, 1),
2289        block_dim: (HVP_THREADS, 1, 1),
2290        shared_mem_bytes: 0,
2291    };
2292    let mut builder = stream.launch_builder(&partial_func);
2293    builder
2294        .arg(&args.n_i32)
2295        .arg(&args.r_i32)
2296        .arg(&args.p_m_i32)
2297        .arg(&args.p_g_i32)
2298        .arg(&args.p_total_i32)
2299        .arg(&args.h_block_start)
2300        .arg(&args.h_block_len)
2301        .arg(&args.w_block_start)
2302        .arg(&args.w_block_len)
2303        .arg(&args.h_primary_start)
2304        .arg(&args.w_primary_start)
2305        .arg(&args.rows_per_cta)
2306        .arg(&storage.neglog)
2307        .arg(&storage.grad)
2308        .arg(&storage.marginal_design)
2309        .arg(&storage.logslope_design)
2310        .arg(&mut d_partial);
2311    // SAFETY: all resident buffers were allocated and shape-validated by the
2312    // row-kernel producer; `d_partial` has `num_chunks * (1+p_total)` entries.
2313    unsafe { builder.launch(cfg_partial) }.map_err(|err| GpuError::DriverCallFailed {
2314        reason: format!("bms_flex_row joint gradient partial launch: {err}"),
2315    })?;
2316
2317    let output_width_i32 = i32::try_from(output_width).map_err(|_| GpuError::DriverCallFailed {
2318        reason: format!(
2319            "bms_flex_row joint gradient: output_width={output_width} exceeds i32 range"
2320        ),
2321    })?;
2322    let num_chunks_i32 =
2323        i32::try_from(args.num_chunks).map_err(|_| GpuError::DriverCallFailed {
2324            reason: format!(
2325                "bms_flex_row joint gradient: num_chunks={} exceeds i32 range",
2326                args.num_chunks
2327            ),
2328        })?;
2329    let output_width_u32 = u32::try_from(output_width).map_err(|_| GpuError::DriverCallFailed {
2330        reason: format!(
2331            "bms_flex_row joint gradient: output_width={output_width} exceeds u32 range"
2332        ),
2333    })?;
2334    let reduce_blocks = output_width_u32.div_ceil(REDUCTION_THREADS);
2335    let cfg_reduce = LaunchConfig {
2336        grid_dim: (reduce_blocks, 1, 1),
2337        block_dim: (REDUCTION_THREADS, 1, 1),
2338        shared_mem_bytes: 0,
2339    };
2340    let mut builder = stream.launch_builder(&reduce_func);
2341    builder
2342        .arg(&num_chunks_i32)
2343        .arg(&output_width_i32)
2344        .arg(&d_partial)
2345        .arg(&mut d_out);
2346    // SAFETY: the partial launch above populated the exact partial shape and
2347    // `d_out` owns `output_width` entries.
2348    unsafe { builder.launch(cfg_reduce) }.map_err(|err| GpuError::DriverCallFailed {
2349        reason: format!("bms_flex_row joint gradient reduce launch: {err}"),
2350    })?;
2351    stream
2352        .synchronize()
2353        .map_err(|err| GpuError::DriverCallFailed {
2354            reason: format!("bms_flex_row joint gradient synchronize: {err}"),
2355        })?;
2356    let host = stream
2357        .clone_dtoh(&d_out)
2358        .map_err(|err| GpuError::DriverCallFailed {
2359            reason: format!("bms_flex_row joint gradient download: {err}"),
2360        })?;
2361    if let Some((index, value)) = host
2362        .iter()
2363        .copied()
2364        .enumerate()
2365        .find(|(_, value)| !value.is_finite())
2366    {
2367        return Err(GpuError::DriverCallFailed {
2368            reason: format!(
2369                "bms_flex_row joint gradient produced non-finite output[{index}]={value}"
2370            ),
2371        });
2372    }
2373    Ok(BmsFlexDeviceJointGradient {
2374        log_likelihood: host[0],
2375        gradient: host[1..].to_vec(),
2376    })
2377}
2378
2379/// Which partial kernel the joint-β engine drives, whether it consumes a
2380/// direction vector `d_v`, and where the reduced `[1, p_total]` image lands.
2381/// All three points of variation are encoded here so the public entry points
2382/// stay thin wrappers over one launch helper.
2383#[cfg(target_os = "linux")]
2384#[derive(Clone, Copy)]
2385pub(crate) enum BmsFlexRowLaunchMode {
2386    /// `bms_flex_row_hvp_partial`, `H · v` per row, result left on-stream.
2387    HvpDeviceOut,
2388    /// `bms_flex_row_diag_partial`, `diag(H)` per row, downloaded to host.
2389    DiagonalHostOut,
2390}
2391
2392#[cfg(target_os = "linux")]
2393impl BmsFlexRowLaunchMode {
2394    /// Name of the partial kernel this mode loads from the HVP module.
2395    pub(crate) fn partial_kernel_name(self) -> &'static str {
2396        match self {
2397            BmsFlexRowLaunchMode::HvpDeviceOut => "bms_flex_row_hvp_partial",
2398            BmsFlexRowLaunchMode::DiagonalHostOut => "bms_flex_row_diag_partial",
2399        }
2400    }
2401}
2402
2403/// All scalar launch arguments for the joint-β partial kernel, derived once
2404/// from a [`DeviceResidentRowHess`]. The HVP and diagonal partial kernels take
2405/// the identical leading block-layout argument list (only the trailing
2406/// `d_v` / output pointers differ), so this captures the long, easy-to-
2407/// desynchronize prefix in a single place.
2408#[cfg(target_os = "linux")]
2409pub(crate) struct PreparedBmsFlexRowLaunchArgs {
2410    pub(crate) n_i32: i32,
2411    pub(crate) r_i32: i32,
2412    pub(crate) p_m_i32: i32,
2413    pub(crate) p_g_i32: i32,
2414    pub(crate) p_total_i32: i32,
2415    pub(crate) h_block_start: i32,
2416    pub(crate) h_block_len: i32,
2417    pub(crate) w_block_start: i32,
2418    pub(crate) w_block_len: i32,
2419    pub(crate) h_primary_start: i32,
2420    pub(crate) w_primary_start: i32,
2421    pub(crate) rows_per_cta: i32,
2422    pub(crate) num_chunks: usize,
2423    pub(crate) num_chunks_i32: i32,
2424    pub(crate) num_chunks_u32: u32,
2425    pub(crate) p_total_u32: u32,
2426}
2427
2428#[cfg(target_os = "linux")]
2429impl PreparedBmsFlexRowLaunchArgs {
2430    pub(crate) fn from_storage(storage: &DeviceResidentRowHess) -> Result<Self, GpuError> {
2431        if storage.n == 0 {
2432            return Err(GpuError::DriverCallFailed {
2433                reason: "bms_flex_row launch: n_rows must be > 0".to_string(),
2434            });
2435        }
2436        if storage.r < 2 {
2437            return Err(GpuError::DriverCallFailed {
2438                reason: format!("bms_flex_row launch: r={} must be >= 2", storage.r),
2439            });
2440        }
2441        let p_total = storage.block.p_total;
2442        if p_total == 0 {
2443            return Err(GpuError::DriverCallFailed {
2444                reason: "bms_flex_row launch: p_total must be > 0".to_string(),
2445            });
2446        }
2447        if storage.primary.r != storage.r {
2448            return Err(GpuError::DriverCallFailed {
2449                reason: format!(
2450                    "bms_flex_row launch: primary.r={} != storage.r={}",
2451                    storage.primary.r, storage.r
2452                ),
2453            });
2454        }
2455        let h_block_len = storage.block.h.as_ref().map_or(0, |range| range.len());
2456        let w_block_len = storage.block.w.as_ref().map_or(0, |range| range.len());
2457        let h_primary_len = storage.primary.h.as_ref().map_or(0, |range| range.len());
2458        let w_primary_len = storage.primary.w.as_ref().map_or(0, |range| range.len());
2459        if h_block_len != h_primary_len || w_block_len != w_primary_len {
2460            return Err(GpuError::DriverCallFailed {
2461                reason: format!(
2462                    "bms_flex_row launch: block/primary direct lengths disagree: h={h_block_len}/{h_primary_len}, w={w_block_len}/{w_primary_len}"
2463                ),
2464            });
2465        }
2466        let h_block_start = storage
2467            .block
2468            .p_m
2469            .checked_add(storage.block.p_g)
2470            .ok_or_else(|| GpuError::DriverCallFailed {
2471                reason: "bms_flex_row launch: p_m+p_g overflow".to_string(),
2472            })?;
2473        let w_block_start =
2474            h_block_start
2475                .checked_add(h_block_len)
2476                .ok_or_else(|| GpuError::DriverCallFailed {
2477                    reason: "bms_flex_row launch: h block end overflow".to_string(),
2478                })?;
2479        let expected_p_total =
2480            w_block_start
2481                .checked_add(w_block_len)
2482                .ok_or_else(|| GpuError::DriverCallFailed {
2483                    reason: "bms_flex_row launch: w block end overflow".to_string(),
2484                })?;
2485        let w_primary_start =
2486            2_usize
2487                .checked_add(h_primary_len)
2488                .ok_or_else(|| GpuError::DriverCallFailed {
2489                    reason: "bms_flex_row launch: h primary end overflow".to_string(),
2490                })?;
2491        let expected_r = w_primary_start.checked_add(w_primary_len).ok_or_else(|| {
2492            GpuError::DriverCallFailed {
2493                reason: "bms_flex_row launch: w primary end overflow".to_string(),
2494            }
2495        })?;
2496        let check_range = |name: &str,
2497                           range: Option<&std::ops::Range<usize>>,
2498                           expected_start: usize,
2499                           expected_len: usize|
2500         -> Result<(), GpuError> {
2501            match (range, expected_len) {
2502                (None, 0) => Ok(()),
2503                (Some(range), len)
2504                    if len > 0
2505                        && range.start == expected_start
2506                        && range.end == expected_start + len =>
2507                {
2508                    Ok(())
2509                }
2510                _ => Err(GpuError::DriverCallFailed {
2511                    reason: format!(
2512                        "bms_flex_row launch: {name}={range:?} must be {expected_start}..{}",
2513                        expected_start + expected_len
2514                    ),
2515                }),
2516            }
2517        };
2518        check_range(
2519            "block.h",
2520            storage.block.h.as_ref(),
2521            h_block_start,
2522            h_block_len,
2523        )?;
2524        check_range(
2525            "block.w",
2526            storage.block.w.as_ref(),
2527            w_block_start,
2528            w_block_len,
2529        )?;
2530        check_range("primary.h", storage.primary.h.as_ref(), 2, h_primary_len)?;
2531        check_range(
2532            "primary.w",
2533            storage.primary.w.as_ref(),
2534            w_primary_start,
2535            w_primary_len,
2536        )?;
2537        if p_total != expected_p_total || storage.r != expected_r {
2538            return Err(GpuError::DriverCallFailed {
2539                reason: format!(
2540                    "bms_flex_row launch: inconsistent layout p_total={p_total}/{expected_p_total}, r={}/{}",
2541                    storage.r, expected_r
2542                ),
2543            });
2544        }
2545        let expected_nr = checked_shape_len("launch storage [n,r]", &[storage.n, storage.r])?;
2546        let expected_nrr =
2547            checked_shape_len("launch storage [n,r,r]", &[storage.n, storage.r, storage.r])?;
2548        let expected_marginal = checked_shape_len(
2549            "launch storage marginal design",
2550            &[storage.n, storage.block.p_m],
2551        )?;
2552        let expected_logslope = checked_shape_len(
2553            "launch storage logslope design",
2554            &[storage.n, storage.block.p_g],
2555        )?;
2556        for (name, have, want) in [
2557            ("neglog", storage.neglog.len(), storage.n),
2558            ("grad", storage.grad.len(), expected_nr),
2559            ("hess", storage.hess.len(), expected_nrr),
2560            (
2561                "marginal_design",
2562                storage.marginal_design.len(),
2563                expected_marginal,
2564            ),
2565            (
2566                "logslope_design",
2567                storage.logslope_design.len(),
2568                expected_logslope,
2569            ),
2570        ] {
2571            if have != want {
2572                return Err(GpuError::DriverCallFailed {
2573                    reason: format!("bms_flex_row launch: storage {name}.len()={have} != {want}"),
2574                });
2575            }
2576        }
2577        let num_chunks = num_hvp_chunks(storage.n);
2578        let to_i32 = |name: &str, value: usize| {
2579            i32::try_from(value).map_err(|_| GpuError::DriverCallFailed {
2580                reason: format!("bms_flex_row launch: {name}={value} exceeds i32 range"),
2581            })
2582        };
2583        let to_u32 = |name: &str, value: usize| {
2584            u32::try_from(value).map_err(|_| GpuError::DriverCallFailed {
2585                reason: format!("bms_flex_row launch: {name}={value} exceeds u32 range"),
2586            })
2587        };
2588        Ok(PreparedBmsFlexRowLaunchArgs {
2589            n_i32: to_i32("n_rows", storage.n)?,
2590            r_i32: to_i32("r", storage.r)?,
2591            p_m_i32: to_i32("p_m", storage.block.p_m)?,
2592            p_g_i32: to_i32("p_g", storage.block.p_g)?,
2593            p_total_i32: to_i32("p_total", p_total)?,
2594            h_block_start: storage
2595                .block
2596                .h
2597                .as_ref()
2598                .map(|range| to_i32("h_block_start", range.start))
2599                .transpose()?
2600                .unwrap_or(0),
2601            h_block_len: storage
2602                .block
2603                .h
2604                .as_ref()
2605                .map(|range| to_i32("h_block_len", range.len()))
2606                .transpose()?
2607                .unwrap_or(0),
2608            w_block_start: storage
2609                .block
2610                .w
2611                .as_ref()
2612                .map(|range| to_i32("w_block_start", range.start))
2613                .transpose()?
2614                .unwrap_or(0),
2615            w_block_len: storage
2616                .block
2617                .w
2618                .as_ref()
2619                .map(|range| to_i32("w_block_len", range.len()))
2620                .transpose()?
2621                .unwrap_or(0),
2622            h_primary_start: storage
2623                .primary
2624                .h
2625                .as_ref()
2626                .map(|range| to_i32("h_primary_start", range.start))
2627                .transpose()?
2628                .unwrap_or(0),
2629            w_primary_start: storage
2630                .primary
2631                .w
2632                .as_ref()
2633                .map(|range| to_i32("w_primary_start", range.start))
2634                .transpose()?
2635                .unwrap_or(0),
2636            rows_per_cta: i32::try_from(HVP_ROWS_PER_CTA).map_err(|_| {
2637                GpuError::DriverCallFailed {
2638                    reason: format!(
2639                        "bms_flex_row launch: rows_per_cta={HVP_ROWS_PER_CTA} exceeds i32 range"
2640                    ),
2641                }
2642            })?,
2643            num_chunks,
2644            num_chunks_i32: to_i32("num_chunks", num_chunks)?,
2645            num_chunks_u32: to_u32("num_chunks", num_chunks)?,
2646            p_total_u32: to_u32("p_total", p_total)?,
2647        })
2648    }
2649}
2650
2651/// Shared partial+reduce engine behind every joint-β launcher.
2652///
2653/// Allocates the `[num_chunks, p_total]` partial buffer, loads the mode's
2654/// partial kernel plus the common `bms_flex_row_hvp_reduce`, builds both
2655/// launch configs from a single [`PreparedBmsFlexRowLaunchArgs`], launches the
2656/// partial kernel (binding `d_v` only for the HVP modes), and launches the
2657/// reduction into caller-supplied `d_out`.
2658///
2659/// **No** `synchronize()` or DtoH is performed here — the surrounding helper
2660/// decides whether to keep the result on-stream (device-resident PCG hot path)
2661/// or sync + download it to the host. `ctx` is a short error-context tag woven
2662/// into every `DriverCallFailed` reason so failures stay attributable to the
2663/// originating entry point.
2664#[cfg(target_os = "linux")]
2665pub(crate) fn run_bms_flex_row_partial_reduce(
2666    storage: &DeviceResidentRowHess,
2667    mode: BmsFlexRowLaunchMode,
2668    d_v: Option<&CudaSlice<f64>>,
2669    d_out: &mut CudaSlice<f64>,
2670    ctx: &str,
2671) -> Result<(), GpuError> {
2672    let backend = HvpKernelBackend::probe()?;
2673    let stream = backend.stream.clone();
2674    let args = PreparedBmsFlexRowLaunchArgs::from_storage(storage)?;
2675    let p_total = storage.block.p_total;
2676
2677    let partial_len = checked_shape_len(
2678        &format!("{ctx} partial [num_chunks,p_total]"),
2679        &[args.num_chunks, p_total],
2680    )?;
2681    let mut d_partial =
2682        stream
2683            .alloc_zeros::<f64>(partial_len)
2684            .map_err(|err| GpuError::DriverCallFailed {
2685                reason: format!("bms_flex_row {ctx} alloc partial: {err}"),
2686            })?;
2687
2688    let partial_kernel_name = mode.partial_kernel_name();
2689    let part_func = backend
2690        .module
2691        .load_function(partial_kernel_name)
2692        .map_err(|err| GpuError::DriverCallFailed {
2693            reason: format!("bms_flex_row {ctx} load {partial_kernel_name}: {err}"),
2694        })?;
2695    let red_func = backend
2696        .module
2697        .load_function("bms_flex_row_hvp_reduce")
2698        .map_err(|err| GpuError::DriverCallFailed {
2699            reason: format!("bms_flex_row {ctx} load reduce: {err}"),
2700        })?;
2701
2702    let cfg_part = LaunchConfig {
2703        grid_dim: (args.num_chunks_u32, 1, 1),
2704        block_dim: (HVP_THREADS, 1, 1),
2705        shared_mem_bytes: 0,
2706    };
2707    let mut builder = stream.launch_builder(&part_func);
2708    builder
2709        .arg(&args.n_i32)
2710        .arg(&args.r_i32)
2711        .arg(&args.p_m_i32)
2712        .arg(&args.p_g_i32)
2713        .arg(&args.p_total_i32)
2714        .arg(&args.h_block_start)
2715        .arg(&args.h_block_len)
2716        .arg(&args.w_block_start)
2717        .arg(&args.w_block_len)
2718        .arg(&args.h_primary_start)
2719        .arg(&args.w_primary_start)
2720        .arg(&args.rows_per_cta)
2721        .arg(&storage.hess)
2722        .arg(&storage.marginal_design)
2723        .arg(&storage.logslope_design);
2724    if let Some(d_v) = d_v {
2725        builder.arg(d_v);
2726    }
2727    builder.arg(&mut d_partial);
2728    // SAFETY: every device pointer above either comes from `storage` (whose
2729    // capacities were established by
2730    // `launch_bms_flex_row_kernel_device_resident`) or was just allocated here
2731    // (`d_partial` = num_chunks * p_total). `d_v`, when bound, is length-checked
2732    // by the calling adapter against `p_total`. The diagonal partial kernel
2733    // takes no direction argument, matching `d_v == None`. Scalar args are i32
2734    // by-value.
2735    unsafe { builder.launch(cfg_part) }.map_err(|err| GpuError::DriverCallFailed {
2736        reason: format!("bms_flex_row {ctx} partial launch: {err}"),
2737    })?;
2738
2739    let red_threads: u32 = REDUCTION_THREADS;
2740    let red_blocks = args.p_total_u32.div_ceil(red_threads);
2741    let cfg_red = LaunchConfig {
2742        grid_dim: (red_blocks, 1, 1),
2743        block_dim: (red_threads, 1, 1),
2744        shared_mem_bytes: 0,
2745    };
2746    let mut builder = stream.launch_builder(&red_func);
2747    builder
2748        .arg(&args.num_chunks_i32)
2749        .arg(&args.p_total_i32)
2750        .arg(&d_partial)
2751        .arg(d_out);
2752    // SAFETY: `d_partial` was just populated by the partial kernel above;
2753    // `d_out` is `p_total` doubles (length-checked / allocated by the calling
2754    // adapter); both scalar args fit i32.
2755    unsafe { builder.launch(cfg_red) }.map_err(|err| GpuError::DriverCallFailed {
2756        reason: format!("bms_flex_row {ctx} reduce launch: {err}"),
2757    })?;
2758    // `d_partial` drops at end of fn; cudarc keeps the alloc alive until the
2759    // stream is done with it, so the reduce kernel completes safely.
2760    drop(d_partial);
2761    Ok(())
2762}
2763
2764/// Host-returning diagonal adapter. HVP host output uses the multi-RHS engine,
2765/// so this function exposes only the one live no-direction mode.
2766#[cfg(target_os = "linux")]
2767pub(crate) fn launch_bms_flex_row_diagonal_host(
2768    storage: &DeviceResidentRowHess,
2769) -> Result<Vec<f64>, GpuError> {
2770    let p_total = storage.block.p_total;
2771    let backend = HvpKernelBackend::probe()?;
2772    let stream = backend.stream.clone();
2773    let mut d_out =
2774        stream
2775            .alloc_zeros::<f64>(p_total)
2776            .map_err(|err| GpuError::DriverCallFailed {
2777                reason: format!("bms_flex_row diag alloc out: {err}"),
2778            })?;
2779
2780    run_bms_flex_row_partial_reduce(
2781        storage,
2782        BmsFlexRowLaunchMode::DiagonalHostOut,
2783        None,
2784        &mut d_out,
2785        "diag",
2786    )?;
2787
2788    stream
2789        .synchronize()
2790        .map_err(|err| GpuError::DriverCallFailed {
2791            reason: format!("bms_flex_row diag synchronize: {err}"),
2792        })?;
2793    stream
2794        .clone_dtoh(&d_out)
2795        .map_err(|err| GpuError::DriverCallFailed {
2796            reason: format!("bms_flex_row diag download out: {err}"),
2797        })
2798}
2799
2800#[cfg(target_os = "linux")]
2801pub(crate) fn validate_bms_flex_row_hvp_multi_shape(
2802    storage: &DeviceResidentRowHess,
2803    rhs_count: usize,
2804    v_rhs_len: usize,
2805    out_len: Option<usize>,
2806    ctx: &str,
2807) -> Result<usize, GpuError> {
2808    if rhs_count == 0 || rhs_count > BMS_FLEX_ROW_HVP_MAX_RHS {
2809        return Err(GpuError::DriverCallFailed {
2810            reason: format!(
2811                "bms_flex_row {ctx}: rhs_count={rhs_count} outside 1..={BMS_FLEX_ROW_HVP_MAX_RHS}"
2812            ),
2813        });
2814    }
2815    let p_total = storage.block.p_total;
2816    let rhs_elems = rhs_count
2817        .checked_mul(p_total)
2818        .ok_or_else(|| GpuError::DriverCallFailed {
2819            reason: format!(
2820                "bms_flex_row {ctx}: rhs_count({rhs_count})*p_total({p_total}) overflow"
2821            ),
2822        })?;
2823    i32::try_from(rhs_elems).map_err(|_| GpuError::DriverCallFailed {
2824        reason: format!(
2825            "bms_flex_row {ctx}: rhs_count({rhs_count})*p_total({p_total})={rhs_elems} exceeds CUDA int indexing range"
2826        ),
2827    })?;
2828    if v_rhs_len != rhs_elems {
2829        return Err(GpuError::DriverCallFailed {
2830            reason: format!(
2831                "bms_flex_row {ctx}: v_rhs.len()={v_rhs_len} != rhs_count({rhs_count})*p_total({p_total})={rhs_elems}"
2832            ),
2833        });
2834    }
2835    if let Some(out_len) = out_len
2836        && out_len != rhs_elems
2837    {
2838        return Err(GpuError::DriverCallFailed {
2839            reason: format!(
2840                "bms_flex_row {ctx}: out.len()={out_len} != rhs_count({rhs_count})*p_total({p_total})={rhs_elems}"
2841            ),
2842        });
2843    }
2844    Ok(rhs_elems)
2845}
2846
2847/// Transient device bytes for a multi-RHS HVP launch, excluding persistent
2848/// row-Hessian/design storage. Scratch scales with
2849/// `rhs_count * num_chunks * p_total`, not `rhs_count * n * r * r`.
2850#[cfg(target_os = "linux")]
2851pub fn bms_flex_row_hvp_multi_scratch_bytes_for_shape(
2852    n: usize,
2853    p_total: usize,
2854    rhs_count: usize,
2855) -> Result<u64, GpuError> {
2856    if rhs_count == 0 || rhs_count > BMS_FLEX_ROW_HVP_MAX_RHS {
2857        return Err(GpuError::DriverCallFailed {
2858            reason: format!(
2859                "bms_flex_row hvp_multi_scratch_bytes: rhs_count={rhs_count} outside 1..={BMS_FLEX_ROW_HVP_MAX_RHS}"
2860            ),
2861        });
2862    }
2863    let num_chunks = num_hvp_chunks(n);
2864    let partial = rhs_count
2865        .checked_mul(num_chunks)
2866        .and_then(|v| v.checked_mul(p_total))
2867        .ok_or_else(|| GpuError::DriverCallFailed {
2868            reason: format!(
2869                "bms_flex_row hvp_multi_scratch_bytes: rhs_count({rhs_count})*num_chunks({num_chunks})*p_total({p_total}) overflow"
2870            ),
2871        })?;
2872    let rhs_vectors = rhs_count
2873        .checked_mul(p_total)
2874        .and_then(|v| v.checked_mul(2))
2875        .ok_or_else(|| GpuError::DriverCallFailed {
2876            reason: format!(
2877                "bms_flex_row hvp_multi_scratch_bytes: 2*rhs_count({rhs_count})*p_total({p_total}) overflow"
2878            ),
2879        })?;
2880    let elems = partial
2881        .checked_add(rhs_vectors)
2882        .ok_or_else(|| GpuError::DriverCallFailed {
2883            reason: "bms_flex_row hvp_multi_scratch_bytes: element count overflow".to_string(),
2884        })?;
2885    let bytes = elems
2886        .checked_mul(std::mem::size_of::<f64>())
2887        .ok_or_else(|| GpuError::DriverCallFailed {
2888            reason: "bms_flex_row hvp_multi_scratch_bytes: byte count overflow".to_string(),
2889        })?;
2890    u64::try_from(bytes).map_err(|_| GpuError::DriverCallFailed {
2891        reason: format!(
2892            "bms_flex_row hvp_multi_scratch_bytes: byte count={bytes} exceeds u64 range"
2893        ),
2894    })
2895}
2896
2897#[cfg(target_os = "linux")]
2898pub(crate) fn run_bms_flex_row_multi_partial_reduce(
2899    storage: &DeviceResidentRowHess,
2900    rhs_count: usize,
2901    d_v_rhs: &CudaSlice<f64>,
2902    d_out: &mut CudaSlice<f64>,
2903    ctx: &str,
2904) -> Result<(), GpuError> {
2905    let rhs_elems = validate_bms_flex_row_hvp_multi_shape(
2906        storage,
2907        rhs_count,
2908        d_v_rhs.len(),
2909        Some(d_out.len()),
2910        ctx,
2911    )?;
2912    let backend = HvpKernelBackend::probe()?;
2913    let stream = backend.stream.clone();
2914    let args = PreparedBmsFlexRowLaunchArgs::from_storage(storage)?;
2915    let p_total = storage.block.p_total;
2916    let partial_len = rhs_count
2917        .checked_mul(args.num_chunks)
2918        .and_then(|v| v.checked_mul(p_total))
2919        .ok_or_else(|| GpuError::DriverCallFailed {
2920            reason: format!(
2921                "bms_flex_row {ctx}: partial length overflow for rhs_count={rhs_count}, num_chunks={}, p_total={p_total}",
2922                args.num_chunks
2923            ),
2924        })?;
2925
2926    let mut d_partial =
2927        stream
2928            .alloc_zeros::<f64>(partial_len)
2929            .map_err(|err| GpuError::DriverCallFailed {
2930                reason: format!("bms_flex_row {ctx} alloc multi partial: {err}"),
2931            })?;
2932    let part_func = backend
2933        .module
2934        .load_function("bms_flex_row_hvp_multi_partial")
2935        .map_err(|err| GpuError::DriverCallFailed {
2936            reason: format!("bms_flex_row {ctx} load multi partial: {err}"),
2937        })?;
2938    let red_func = backend
2939        .module
2940        .load_function("bms_flex_row_hvp_multi_reduce")
2941        .map_err(|err| GpuError::DriverCallFailed {
2942            reason: format!("bms_flex_row {ctx} load multi reduce: {err}"),
2943        })?;
2944
2945    let rhs_count_i32 = i32::try_from(rhs_count).map_err(|_| GpuError::DriverCallFailed {
2946        reason: format!("bms_flex_row {ctx}: rhs_count={rhs_count} exceeds i32 range"),
2947    })?;
2948    let cfg_part = LaunchConfig {
2949        grid_dim: (args.num_chunks_u32, 1, 1),
2950        block_dim: (HVP_THREADS, 1, 1),
2951        shared_mem_bytes: 0,
2952    };
2953    let mut builder = stream.launch_builder(&part_func);
2954    builder
2955        .arg(&args.n_i32)
2956        .arg(&args.r_i32)
2957        .arg(&args.p_m_i32)
2958        .arg(&args.p_g_i32)
2959        .arg(&args.p_total_i32)
2960        .arg(&args.h_block_start)
2961        .arg(&args.h_block_len)
2962        .arg(&args.w_block_start)
2963        .arg(&args.w_block_len)
2964        .arg(&args.h_primary_start)
2965        .arg(&args.w_primary_start)
2966        .arg(&args.rows_per_cta)
2967        .arg(&rhs_count_i32)
2968        .arg(&storage.hess)
2969        .arg(&storage.marginal_design)
2970        .arg(&storage.logslope_design)
2971        .arg(d_v_rhs)
2972        .arg(&mut d_partial);
2973    // SAFETY: storage buffers were validated at construction; `d_v_rhs` and
2974    // `d_out` have rhs_count*p_total elements, `d_partial` has
2975    // rhs_count*num_chunks*p_total, and rhs_count is bounded by fixed shared
2976    // array sizes in the CUDA source.
2977    unsafe { builder.launch(cfg_part) }.map_err(|err| GpuError::DriverCallFailed {
2978        reason: format!("bms_flex_row {ctx} multi partial launch: {err}"),
2979    })?;
2980
2981    let red_threads: u32 = REDUCTION_THREADS;
2982    let rhs_elems_u32 = u32::try_from(rhs_elems).map_err(|_| GpuError::DriverCallFailed {
2983        reason: format!("bms_flex_row {ctx}: rhs elements={rhs_elems} exceed u32 range"),
2984    })?;
2985    let red_blocks = rhs_elems_u32.div_ceil(red_threads);
2986    let cfg_red = LaunchConfig {
2987        grid_dim: (red_blocks, 1, 1),
2988        block_dim: (red_threads, 1, 1),
2989        shared_mem_bytes: 0,
2990    };
2991    let mut builder = stream.launch_builder(&red_func);
2992    builder
2993        .arg(&args.num_chunks_i32)
2994        .arg(&args.p_total_i32)
2995        .arg(&rhs_count_i32)
2996        .arg(&d_partial)
2997        .arg(d_out);
2998    // SAFETY: the reduce kernel reads the just-populated partial buffer and
2999    // writes exactly rhs_count*p_total output entries.
3000    unsafe { builder.launch(cfg_red) }.map_err(|err| GpuError::DriverCallFailed {
3001        reason: format!("bms_flex_row {ctx} multi reduce launch: {err}"),
3002    })?;
3003    drop(d_partial);
3004    Ok(())
3005}
3006
3007/// Device-resident multi-RHS HVP. `v_rhs` is row-major
3008/// `[rhs_count, p_total]`; the returned vector has the same layout.
3009#[cfg(target_os = "linux")]
3010pub(crate) fn launch_bms_flex_row_hvp_multi(
3011    storage: &DeviceResidentRowHess,
3012    v_rhs: &[f64],
3013    rhs_count: usize,
3014) -> Result<Vec<f64>, GpuError> {
3015    let rhs_elems =
3016        validate_bms_flex_row_hvp_multi_shape(storage, rhs_count, v_rhs.len(), None, "hvp_multi")?;
3017    let backend = HvpKernelBackend::probe()?;
3018    let stream = backend.stream.clone();
3019    let d_v_rhs = stream
3020        .clone_htod(v_rhs)
3021        .map_err(|err| GpuError::DriverCallFailed {
3022            reason: format!("bms_flex_row hvp_multi upload v_rhs: {err}"),
3023        })?;
3024    let mut d_out =
3025        stream
3026            .alloc_zeros::<f64>(rhs_elems)
3027            .map_err(|err| GpuError::DriverCallFailed {
3028                reason: format!("bms_flex_row hvp_multi alloc out: {err}"),
3029            })?;
3030    run_bms_flex_row_multi_partial_reduce(storage, rhs_count, &d_v_rhs, &mut d_out, "hvp_multi")?;
3031    stream
3032        .synchronize()
3033        .map_err(|err| GpuError::DriverCallFailed {
3034            reason: format!("bms_flex_row hvp_multi synchronize: {err}"),
3035        })?;
3036    stream
3037        .clone_dtoh(&d_out)
3038        .map_err(|err| GpuError::DriverCallFailed {
3039            reason: format!("bms_flex_row hvp_multi download out: {err}"),
3040        })
3041}
3042
3043/// Materialize a row-major dense matrix from batched column images `H * I`.
3044/// Each launcher input/output is row-major `[rhs_count, p_total]`; the output
3045/// vectors are columns of `H`, so this routine performs the one required
3046/// transpose while copying them into `[row, column]` storage.
3047#[cfg(target_os = "linux")]
3048fn materialize_dense_from_hvp_batches(
3049    p_total: usize,
3050    mut launch: impl FnMut(&[f64], usize) -> Result<Vec<f64>, GpuError>,
3051) -> Result<Vec<f64>, GpuError> {
3052    if p_total == 0 {
3053        return Err(GpuError::DriverCallFailed {
3054            reason: "bms_flex_row dense HVP materialization: p_total must be > 0".to_string(),
3055        });
3056    }
3057    let dense_len = p_total
3058        .checked_mul(p_total)
3059        .ok_or_else(|| GpuError::DriverCallFailed {
3060            reason: format!(
3061                "bms_flex_row dense HVP materialization: p_total={p_total} square overflow"
3062            ),
3063        })?;
3064    let mut dense = vec![0.0_f64; dense_len];
3065    for column_start in (0..p_total).step_by(BMS_FLEX_ROW_HVP_MAX_RHS) {
3066        let rhs_count = (p_total - column_start).min(BMS_FLEX_ROW_HVP_MAX_RHS);
3067        let batch_len = checked_shape_len(
3068            "dense HVP materialization [rhs_count,p_total]",
3069            &[rhs_count, p_total],
3070        )?;
3071        let mut basis = vec![0.0_f64; batch_len];
3072        for local_column in 0..rhs_count {
3073            basis[local_column * p_total + column_start + local_column] = 1.0;
3074        }
3075        let images = launch(&basis, rhs_count)?;
3076        if images.len() != basis.len() {
3077            return Err(GpuError::DriverCallFailed {
3078                reason: format!(
3079                    "bms_flex_row dense HVP materialization: batch at column {column_start} returned {} values, expected {}",
3080                    images.len(),
3081                    basis.len()
3082                ),
3083            });
3084        }
3085        for local_column in 0..rhs_count {
3086            let column = column_start + local_column;
3087            let image = &images[local_column * p_total..(local_column + 1) * p_total];
3088            for (row, &value) in image.iter().enumerate() {
3089                dense[row * p_total + column] = value;
3090            }
3091        }
3092    }
3093    Ok(dense)
3094}
3095
3096/// Device-output HVP. Runs `bms_flex_row_hvp_partial` +
3097/// `bms_flex_row_hvp_reduce` on the storage's stream against caller-supplied
3098/// device-resident `d_v` (length `p_total` doubles), writing the result into
3099/// caller-supplied `d_out` (also `p_total` doubles). **No** `synchronize()`
3100/// or DtoH is performed — the caller is responsible for stream ordering
3101/// against any consumer that reads `d_out`.
3102///
3103/// This is the device-resident PCG hot path (Block 9 Phase 5): keeping the
3104/// HVP output on the stream lets the outer PCG loop chain axpy / dot /
3105/// preconditioner kernels back-to-back without a per-iter device sync.
3106#[cfg(target_os = "linux")]
3107pub(crate) fn launch_bms_flex_row_hvp_into_device(
3108    storage: &DeviceResidentRowHess,
3109    d_v: &CudaSlice<f64>,
3110    d_out: &mut CudaSlice<f64>,
3111) -> Result<(), GpuError> {
3112    let p_total = storage.block.p_total;
3113    if d_v.len() != p_total {
3114        return Err(GpuError::DriverCallFailed {
3115            reason: format!(
3116                "bms_flex_row hvp_into_device: d_v.len()={} != p_total={}",
3117                d_v.len(),
3118                p_total
3119            ),
3120        });
3121    }
3122    if d_out.len() != p_total {
3123        return Err(GpuError::DriverCallFailed {
3124            reason: format!(
3125                "bms_flex_row hvp_into_device: d_out.len()={} != p_total={}",
3126                d_out.len(),
3127                p_total
3128            ),
3129        });
3130    }
3131    // On-stream output: the shared engine launches partial+reduce into the
3132    // caller's `d_out` and returns without sync/DtoH, so the outer PCG loop can
3133    // chain device kernels against the result.
3134    run_bms_flex_row_partial_reduce(
3135        storage,
3136        BmsFlexRowLaunchMode::HvpDeviceOut,
3137        Some(d_v),
3138        d_out,
3139        "hvp_into_device",
3140    )
3141}
3142
3143/// Launch the device-resident HVP kernel. Returns the host-side joint β image
3144/// of length `block.p_total`.
3145#[cfg(target_os = "linux")]
3146pub(crate) fn launch_bms_flex_row_hvp(
3147    storage: &DeviceResidentRowHess,
3148    v: &[f64],
3149) -> Result<Vec<f64>, GpuError> {
3150    launch_bms_flex_row_hvp_multi(storage, v, 1)
3151}
3152
3153/// Launch the device-resident diagonal kernel. Returns the host-side joint
3154/// β diagonal of length `block.p_total`.
3155#[cfg(target_os = "linux")]
3156pub(crate) fn launch_bms_flex_row_diagonal(
3157    storage: &DeviceResidentRowHess,
3158) -> Result<Vec<f64>, GpuError> {
3159    launch_bms_flex_row_diagonal_host(storage)
3160}
3161
3162/// Block 9 Phase 6 — hard cap on `p_total` for the dense joint-Hessian
3163/// device kernel. Per-CTA shared-memory accumulator is `p_total² * 8`
3164/// bytes. V100 default per-block shared cap is 48 KiB, so the largest
3165/// safe `p_total` here is `sqrt(48 KiB / 8) = 78`. We round down to a
3166/// power-of-two-ish multiple of 8 for predictable launch geometry.
3167#[cfg(target_os = "linux")]
3168pub(crate) const DENSE_BLOCK_MAX_P: usize = 72;
3169
3170/// Number of rows each dense-block CTA processes. Smaller than the HVP
3171/// `HVP_ROWS_PER_CTA = 256` because the per-row inner loop is `O(r² *
3172/// (p_m + p_g + h_block_len + w_block_len))` rather than `O(r²)` — fewer
3173/// rows per CTA keeps the per-CTA wall time short and lets us scale grid
3174/// occupancy with `num_chunks = ceil(n / DENSE_BLOCK_ROWS_PER_CTA)`.
3175#[cfg(target_os = "linux")]
3176pub(crate) const DENSE_BLOCK_ROWS_PER_CTA: u32 = 32;
3177
3178/// Materialize the selected device-resident joint Hessian using the fastest
3179/// CUDA algorithm supported by its width. The direct shared-memory kernel is
3180/// used through [`DENSE_BLOCK_MAX_P`]; wider matrices are formed as batched
3181/// `H * I` column images through the existing bounded multi-RHS HVP kernel.
3182/// This is an up-front device algorithm choice, not a CUDA-to-CPU fallback.
3183#[cfg(target_os = "linux")]
3184pub(crate) fn launch_bms_flex_row_dense(
3185    storage: &DeviceResidentRowHess,
3186) -> Result<Vec<f64>, GpuError> {
3187    let p_total = storage.block.p_total;
3188    if p_total <= DENSE_BLOCK_MAX_P {
3189        return launch_bms_flex_row_dense_block(storage);
3190    }
3191    materialize_dense_from_hvp_batches(p_total, |basis, rhs_count| {
3192        launch_bms_flex_row_hvp_multi(storage, basis, rhs_count)
3193    })
3194}
3195
3196/// Launch the Phase-6 dense joint-Hessian block kernel. Returns the
3197/// host-side `[p_total, p_total]` row-major joint H as a `Vec<f64>`
3198/// (length `p_total²`).
3199///
3200/// **Not the default Newton path.** Production Newton uses HVP (Phase 2)
3201/// and never materialises the full dense Hessian. This entry exists for:
3202///   * exact-REML logdet (`log|H|`) when the unified evaluator wants to
3203///     factor H directly instead of going through the matrix-free path;
3204///   * diagnostic dumps that compare the GPU dense build against the CPU
3205///     `BernoulliMarginalSlopeFamily::fused_gradient_dense` reference;
3206///   * small-`p` debug routes where it is cheaper to factor + solve dense
3207///     than to run a PCG.
3208///
3209/// The kernel rejects `p_total > DENSE_BLOCK_MAX_P` cleanly because the
3210/// per-CTA shared-memory accumulator (`p_total² * 8` bytes) would exceed
3211/// the V100 48 KiB/block cap above that threshold.
3212#[cfg(target_os = "linux")]
3213pub fn launch_bms_flex_row_dense_block(
3214    storage: &DeviceResidentRowHess,
3215) -> Result<Vec<f64>, GpuError> {
3216    let p_total = storage.block.p_total;
3217    if p_total == 0 {
3218        return Err(GpuError::DriverCallFailed {
3219            reason: "bms_flex_row dense_block: p_total must be > 0".to_string(),
3220        });
3221    }
3222    if p_total > DENSE_BLOCK_MAX_P {
3223        return Err(GpuError::DriverCallFailed {
3224            reason: format!(
3225                "bms_flex_row dense_block: p_total={p_total} exceeds DENSE_BLOCK_MAX_P={DENSE_BLOCK_MAX_P} \
3226                 (per-CTA shmem accumulator p²*8 bytes would exceed V100's 48 KiB/block)"
3227            ),
3228        });
3229    }
3230    let backend = HvpKernelBackend::probe()?;
3231    let stream = backend.stream.clone();
3232    let args = PreparedBmsFlexRowLaunchArgs::from_storage(storage)?;
3233    let n = storage.n;
3234    let rows_per_cta = DENSE_BLOCK_ROWS_PER_CTA as usize;
3235    let num_chunks = n.div_ceil(rows_per_cta);
3236    let pp = checked_shape_len("dense_block [p_total,p_total]", &[p_total, p_total])?;
3237    let partial_len = checked_shape_len("dense_block partial", &[num_chunks, pp])?;
3238
3239    let mut d_partial =
3240        stream
3241            .alloc_zeros::<f64>(partial_len)
3242            .map_err(|err| GpuError::DriverCallFailed {
3243                reason: format!("bms_flex_row dense_block alloc partial: {err}"),
3244            })?;
3245    let mut d_out = stream
3246        .alloc_zeros::<f64>(pp)
3247        .map_err(|err| GpuError::DriverCallFailed {
3248            reason: format!("bms_flex_row dense_block alloc out: {err}"),
3249        })?;
3250
3251    let part_func = backend
3252        .module
3253        .load_function("bms_flex_row_dense_block_partial")
3254        .map_err(|err| GpuError::DriverCallFailed {
3255            reason: format!("bms_flex_row dense_block load partial: {err}"),
3256        })?;
3257    let red_func = backend
3258        .module
3259        .load_function("bms_flex_row_dense_block_reduce")
3260        .map_err(|err| GpuError::DriverCallFailed {
3261            reason: format!("bms_flex_row dense_block load reduce: {err}"),
3262        })?;
3263
3264    let rows_per_cta_i32 = i32::try_from(DENSE_BLOCK_ROWS_PER_CTA).map_err(|_| {
3265        GpuError::DriverCallFailed {
3266            reason: format!(
3267                "bms_flex_row dense_block: rows_per_cta={DENSE_BLOCK_ROWS_PER_CTA} exceeds i32 range"
3268            ),
3269        }
3270    })?;
3271    let num_chunks_u32 = u32::try_from(num_chunks).map_err(|_| GpuError::DriverCallFailed {
3272        reason: format!("bms_flex_row dense_block: num_chunks={num_chunks} exceeds u32 range"),
3273    })?;
3274    let num_chunks_i32 = i32::try_from(num_chunks).map_err(|_| GpuError::DriverCallFailed {
3275        reason: format!("bms_flex_row dense_block: num_chunks={num_chunks} exceeds i32 range"),
3276    })?;
3277    let pp_u32 = u32::try_from(pp).map_err(|_| GpuError::DriverCallFailed {
3278        reason: format!("bms_flex_row dense_block: p_total²={pp} exceeds u32 range"),
3279    })?;
3280
3281    // Per-CTA shmem accumulator: p_total² doubles.
3282    let shmem_bytes_usize =
3283        pp.checked_mul(std::mem::size_of::<f64>())
3284            .ok_or_else(|| GpuError::DriverCallFailed {
3285                reason: format!("dense_block shmem bytes overflow for p_total={p_total}"),
3286            })?;
3287    let shmem_bytes: u32 =
3288        u32::try_from(shmem_bytes_usize).map_err(|_| GpuError::DriverCallFailed {
3289            reason: format!("dense_block shmem bytes overflow u32 for p_total={p_total}"),
3290        })?;
3291
3292    let cfg_part = LaunchConfig {
3293        grid_dim: (num_chunks_u32, 1, 1),
3294        block_dim: (HVP_THREADS, 1, 1),
3295        shared_mem_bytes: shmem_bytes,
3296    };
3297    let mut builder = stream.launch_builder(&part_func);
3298    builder
3299        .arg(&args.n_i32)
3300        .arg(&args.r_i32)
3301        .arg(&args.p_m_i32)
3302        .arg(&args.p_g_i32)
3303        .arg(&args.p_total_i32)
3304        .arg(&args.h_block_start)
3305        .arg(&args.h_block_len)
3306        .arg(&args.w_block_start)
3307        .arg(&args.w_block_len)
3308        .arg(&args.h_primary_start)
3309        .arg(&args.w_primary_start)
3310        .arg(&rows_per_cta_i32)
3311        .arg(&storage.hess)
3312        .arg(&storage.marginal_design)
3313        .arg(&storage.logslope_design)
3314        .arg(&mut d_partial);
3315    // SAFETY: storage pointers have validated capacities; d_partial sized
3316    // num_chunks * pp doubles; dynamic shmem matches the kernel's `extern
3317    // __shared__` accumulator length.
3318    unsafe { builder.launch(cfg_part) }.map_err(|err| GpuError::DriverCallFailed {
3319        reason: format!("bms_flex_row dense_block partial launch: {err}"),
3320    })?;
3321
3322    let red_threads: u32 = REDUCTION_THREADS;
3323    let red_blocks = pp_u32.div_ceil(red_threads);
3324    let cfg_red = LaunchConfig {
3325        grid_dim: (red_blocks, 1, 1),
3326        block_dim: (red_threads, 1, 1),
3327        shared_mem_bytes: 0,
3328    };
3329    let mut builder = stream.launch_builder(&red_func);
3330    builder
3331        .arg(&num_chunks_i32)
3332        .arg(&args.p_total_i32)
3333        .arg(&d_partial)
3334        .arg(&mut d_out);
3335    // SAFETY: d_partial just populated, d_out is pp doubles.
3336    unsafe { builder.launch(cfg_red) }.map_err(|err| GpuError::DriverCallFailed {
3337        reason: format!("bms_flex_row dense_block reduce launch: {err}"),
3338    })?;
3339    stream
3340        .synchronize()
3341        .map_err(|err| GpuError::DriverCallFailed {
3342            reason: format!("bms_flex_row dense_block sync: {err}"),
3343        })?;
3344    stream
3345        .clone_dtoh(&d_out)
3346        .map_err(|err| GpuError::DriverCallFailed {
3347            reason: format!("bms_flex_row dense_block download: {err}"),
3348        })
3349}
3350
3351// Host numerical primitives and the production CPU↔generated-CUDA parity lock.
3352#[cfg(test)]
3353mod row_kernel_tests {
3354    pub(crate) fn host_log_ndtr_and_mills(x: f64) -> (f64, f64) {
3355        gam_gpu::numerics_host::log_ndtr_and_mills(x)
3356    }
3357
3358    // Sole consumer is the `cfg(all(test, target_os = "linux"))` device-test
3359    // module below; off-Linux this would be dead code under `-D warnings`.
3360    #[cfg(target_os = "linux")]
3361    pub(crate) fn host_log_ndtr_mills_curvature(x: f64) -> (f64, f64, f64) {
3362        gam_gpu::numerics_host::log_ndtr_mills_curvature(x)
3363    }
3364
3365    // #415 parity lock: one fitted StandardNormal FLEX family supplies both
3366    // the production CPU lowering and the generated CUDA launch.
3367    pub(crate) mod parity_415 {
3368        use crate::bms::family::*;
3369        use crate::bms::hessian_paths::*;
3370        use crate::bms::{DeviationBlockConfig, LatentMeasureKind, exact_kernel};
3371        use gam_linalg::matrix::{DenseDesignMatrix, DesignMatrix};
3372        use gam_problem::{InverseLink, ParameterBlockState, StandardLink};
3373        use ndarray::{Array1, Array2};
3374        use std::sync::{Arc, Mutex};
3375
3376        /// Build a small but REAL flex BMS family in the `StandardNormal`
3377        /// latent-measure branch with BOTH a score-warp (`p_h > 0`) and a
3378        /// link-deviation (`p_w > 0`) block active, plus mixed labels y ∈ {0,1}.
3379        /// Ported from the `gradient_paths` flex oracle fixture so the cache is
3380        /// populated by the production cell-moment assembly (never hand-faked).
3381        pub(crate) fn make_flex_parity_family(
3382            n: usize,
3383            score_internal_knots: usize,
3384            link_internal_knots: usize,
3385        ) -> (BernoulliMarginalSlopeFamily, Vec<ParameterBlockState>) {
3386            let score_seed = Array1::linspace(-2.0, 2.0, n.max(6));
3387            let link_seed = Array1::linspace(-1.8, 1.8, n.max(6));
3388            let score_cfg = DeviationBlockConfig {
3389                num_internal_knots: score_internal_knots,
3390                ..DeviationBlockConfig::default()
3391            };
3392            let link_cfg = DeviationBlockConfig {
3393                num_internal_knots: link_internal_knots,
3394                ..DeviationBlockConfig::default()
3395            };
3396            let score_prepared =
3397                build_score_warp_deviation_block_from_seed(&score_seed, &score_cfg)
3398                    .expect("build score warp block");
3399            let link_prepared = build_link_deviation_block_from_knots_design_seed_and_weights(
3400                &link_seed, &link_seed, &link_cfg,
3401            )
3402            .expect("build link deviation block");
3403
3404            // Mixed labels y ∈ {0,1} so both s_y = ±1 Mills branches are exercised.
3405            let y: Array1<f64> =
3406                Array1::from_iter((0..n).map(|i| if (i * 17 + 3) % 7 >= 4 { 1.0 } else { 0.0 }));
3407            let weights: Array1<f64> =
3408                Array1::from_iter((0..n).map(|i| 0.75 + ((i * 11 + 5) % 5) as f64 * 0.05));
3409            let z: Array1<f64> =
3410                Array1::from_iter((0..n).map(|i| -1.7 + 3.4 * (i as f64 + 0.5) / n as f64));
3411            let marginal_x = Array2::from_shape_fn((n, 2), |(i, j)| {
3412                if j == 0 {
3413                    1.0
3414                } else {
3415                    -0.4 + 0.8 * ((i * 19 + 7) % n) as f64 / n as f64
3416                }
3417            });
3418            let logslope_x = Array2::from_shape_fn((n, 2), |(i, j)| {
3419                if j == 0 {
3420                    1.0
3421                } else {
3422                    0.3 - 0.6 * ((i * 23 + 11) % n) as f64 / n as f64
3423                }
3424            });
3425
3426            let family = BernoulliMarginalSlopeFamily {
3427                y: Arc::new(y),
3428                weights: Arc::new(weights),
3429                z: Arc::new(z.clone()),
3430                latent_measure: LatentMeasureKind::StandardNormal,
3431                gaussian_frailty_sd: Some(0.15),
3432                base_link: InverseLink::Standard(StandardLink::Probit),
3433                marginal_design: DesignMatrix::Dense(DenseDesignMatrix::from(marginal_x.clone())),
3434                logslope_design: DesignMatrix::Dense(DenseDesignMatrix::from(logslope_x.clone())),
3435                score_warp: Some(score_prepared.runtime.clone()),
3436                link_dev: Some(link_prepared.runtime.clone()),
3437                policy: gam_runtime::resource::ResourcePolicy::default_library(),
3438                cell_moment_lru: Arc::new(exact_kernel::CellMomentLruCache::new(1024)),
3439                cell_moment_cache_stats: Arc::new(exact_kernel::CellMomentCacheStats::default()),
3440                intercept_warm_starts: None,
3441                auto_subsample_phase_counter: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
3442                auto_subsample_last_rho: Arc::new(Mutex::new(None)),
3443            };
3444
3445            let beta_m = Array1::from_vec(vec![0.12, -0.04]);
3446            let beta_g = Array1::from_vec(vec![0.35, 0.03]);
3447            let beta_h = Array1::from_iter(
3448                (0..score_prepared.runtime.basis_dim()).map(|idx| 0.0015 * (idx as f64 + 1.0)),
3449            );
3450            let beta_w = Array1::from_iter(
3451                (0..link_prepared.runtime.basis_dim()).map(|idx| -0.001 * (idx as f64 + 1.0)),
3452            );
3453            let states = vec![
3454                ParameterBlockState {
3455                    eta: marginal_x.dot(&beta_m),
3456                    beta: beta_m,
3457                },
3458                ParameterBlockState {
3459                    eta: logslope_x.dot(&beta_g),
3460                    beta: beta_g,
3461                },
3462                ParameterBlockState {
3463                    beta: beta_h,
3464                    eta: Array1::zeros(z.len()),
3465                },
3466                ParameterBlockState {
3467                    beta: beta_w,
3468                    eta: Array1::zeros(z.len()),
3469                },
3470            ];
3471            (family, states)
3472        }
3473
3474        /// One real StandardNormal full-FLEX fit drives both the production CPU
3475        /// lowering and the generated CUDA kernel. No mirrored host algebra is
3476        /// involved.
3477        fn assert_generated_cuda_row_kernel_matches_canonical_cpu_lowering(
3478            n: usize,
3479            score_internal_knots: usize,
3480            link_internal_knots: usize,
3481            expected_r: Option<usize>,
3482        ) {
3483            let (family, states) =
3484                make_flex_parity_family(n, score_internal_knots, link_internal_knots);
3485            let cache = family
3486                .build_exact_eval_cache(&states)
3487                .expect("flex exact eval cache");
3488            assert!(
3489                cache.row_cell_moments.is_some(),
3490                "#415 fixture must materialise production row-cell moments"
3491            );
3492            let primary = &cache.primary;
3493            let r = primary.total;
3494            let p_h = primary.h.as_ref().map(|range| range.len()).unwrap_or(0);
3495            let p_w = primary.w.as_ref().map(|range| range.len()).unwrap_or(0);
3496            assert!(
3497                p_h > 0 && p_w > 0,
3498                "fixture must activate both deviation blocks"
3499            );
3500            assert_eq!(r, 2 + p_h + p_w);
3501            if let Some(expected_r) = expected_r {
3502                assert_eq!(
3503                    r, expected_r,
3504                    "fixture knot counts must exercise the requested primary width"
3505                );
3506            }
3507
3508            let owned = family
3509                .pack_bms_flex_row_kernel_inputs(&states, &cache)
3510                .expect("packing production CUDA inputs must not error")
3511                .expect("StandardNormal full-FLEX fixture must admit the CUDA row kernel");
3512            let inputs = owned.as_borrowed();
3513            let mut canonical_neglog = vec![0.0; n];
3514            let mut canonical_grad = vec![0.0; n * r];
3515            let mut canonical_hess = vec![0.0; n * r * r];
3516            let mut scratch = BernoulliMarginalSlopeFlexRowScratch::new(r);
3517            let mut checked_labels = [false, false];
3518
3519            for row in 0..n {
3520                let row_ctx = BernoulliMarginalSlopeFamily::row_ctx(&cache, row);
3521                let row_moments = cache
3522                    .row_cell_moments
3523                    .as_ref()
3524                    .and_then(|bundle| bundle.row(row, 9));
3525                assert!(
3526                    row_moments.is_some(),
3527                    "row {row} must carry degree-9 moments"
3528                );
3529                canonical_neglog[row] = family
3530                    .lower_bms_flex_row_order2_with_moments(
3531                        row,
3532                        &states,
3533                        primary,
3534                        row_ctx,
3535                        row_moments,
3536                        cache.cell_family_forest.as_ref(),
3537                        true,
3538                        &mut scratch,
3539                    )
3540                    .expect("canonical production CPU row lowering");
3541                for u in 0..r {
3542                    canonical_grad[row * r + u] = scratch.grad[u];
3543                    for v in 0..r {
3544                        let value = scratch.hess[[u, v]];
3545                        assert!(value.is_finite(), "row {row}: H[{u},{v}] is non-finite");
3546                        assert_eq!(
3547                            value.to_bits(),
3548                            scratch.hess[[v, u]].to_bits(),
3549                            "row {row}: canonical Hessian lost exact symmetry"
3550                        );
3551                        canonical_hess[row * r * r + u * r + v] = value;
3552                    }
3553                }
3554                checked_labels[family.y[row] as usize] = true;
3555            }
3556            assert!(checked_labels[0] && checked_labels[1]);
3557
3558            let mut separates_value_from_q_derivative = false;
3559            for row in 0..n {
3560                let sign = 2.0 * inputs.y[row] - 1.0;
3561                let (_, lambda) = super::host_log_ndtr_and_mills(sign * inputs.e_obs[row]);
3562                let scale = -inputs.w[row] * sign * lambda;
3563                if scale.abs() > 1e-12 {
3564                    let observed_q_derivative = canonical_grad[row * r] / scale;
3565                    if (observed_q_derivative - inputs.e_obs[row]).abs() > 1e-8 {
3566                        separates_value_from_q_derivative = true;
3567                        break;
3568                    }
3569                }
3570            }
3571            assert!(
3572                separates_value_from_q_derivative,
3573                "fixture must distinguish the observed value from its q derivative"
3574            );
3575
3576            #[cfg(not(target_os = "linux"))]
3577            {
3578                eprintln!("[bms_flex_row parity] generated CUDA check requires Linux");
3579                return;
3580            }
3581            #[cfg(target_os = "linux")]
3582            {
3583                match gam_gpu::device_runtime::GpuRuntime::resolve(gam_gpu::GpuPolicy::Auto) {
3584                    Ok(Some(_)) => {}
3585                    Ok(None) => {
3586                        eprintln!("[bms_flex_row parity] no CUDA device");
3587                        return;
3588                    }
3589                    Err(error) => panic!("[bms_flex_row parity] CUDA probe failed: {error}"),
3590                }
3591                let gpu = super::super::launch_bms_flex_row_kernel(owned.as_borrowed())
3592                    .expect("CUDA-selected canonical parity launch must succeed");
3593                let check = |channel: &str, index: usize, cpu: f64, device: f64| {
3594                    let difference = (cpu - device).abs();
3595                    let tolerance = 1e-8 + 1e-8 * cpu.abs();
3596                    assert!(
3597                        difference <= tolerance,
3598                        "{channel}[{index}] CPU={cpu:.17e} CUDA={device:.17e} \
3599                         difference={difference:.3e} tolerance={tolerance:.3e}"
3600                    );
3601                };
3602                for (index, (&cpu, &device)) in
3603                    canonical_neglog.iter().zip(gpu.neglog.iter()).enumerate()
3604                {
3605                    check("neglog", index, cpu, device);
3606                }
3607                for (index, (&cpu, &device)) in
3608                    canonical_grad.iter().zip(gpu.grad.iter()).enumerate()
3609                {
3610                    check("gradient", index, cpu, device);
3611                }
3612                for (index, (&cpu, &device)) in
3613                    canonical_hess.iter().zip(gpu.hess.iter()).enumerate()
3614                {
3615                    check("hessian", index, cpu, device);
3616                }
3617            }
3618        }
3619
3620        #[test]
3621        fn generated_cuda_row_kernel_matches_canonical_cpu_lowering_415() {
3622            assert_generated_cuda_row_kernel_matches_canonical_cpu_lowering(12, 3, 3, None);
3623        }
3624
3625        #[test]
3626        fn full_flex_canonical_exact_cache_admits_material_finite_cell_curvature_2321() {
3627            let (family, states) = make_flex_parity_family(256, 8, 6);
3628            let cache = family
3629                .build_exact_eval_cache(&states)
3630                .expect("the full-FLEX host cache must preserve non-affine finite cells");
3631
3632            let score_width = cache
3633                .primary
3634                .h
3635                .as_ref()
3636                .expect("the full-FLEX fixture must retain its score-warp block")
3637                .len();
3638            let deviation_width = cache
3639                .primary
3640                .w
3641                .as_ref()
3642                .expect("the full-FLEX fixture must retain its link-deviation block")
3643                .len();
3644            assert!(score_width > 0 && deviation_width > 0);
3645            assert_eq!(
3646                cache.primary.total,
3647                2 + score_width + deviation_width,
3648                "the canonical primary layout must contain exactly q, logslope, score-warp, and link-deviation coordinates"
3649            );
3650            assert!(
3651                cache.row_cell_moments.is_some(),
3652                "the production full-FLEX fixture must materialize its exact row-cell cache"
3653            );
3654        }
3655
3656        #[test]
3657        fn generated_cuda_row_kernel_r33_matches_canonical_cpu_lowering_932() {
3658            // Cubic deviation runtimes expose `num_internal_knots + 1` live
3659            // controls since the #2319 knot-selection orbit canonicalization
3660            // (one control fewer per block than the pre-orbit layout this
3661            // fixture was written against). These unequal blocks therefore
3662            // give p_h=16, p_w=15, r=33 — the width just past the 32-lane
3663            // warp boundary this regression exists to exercise.
3664            assert_generated_cuda_row_kernel_matches_canonical_cpu_lowering(40, 15, 14, Some(33));
3665        }
3666    }
3667}
3668
3669#[cfg(all(test, target_os = "linux"))]
3670mod tests {
3671    use super::row_kernel_tests::*;
3672    use super::*;
3673    use crate::bms::exact_eval_cache::RowPrimaryEvalCache;
3674    use crate::bms::row_kernel::BernoulliMarginalSlopeExactNewtonJointHessianWorkspace;
3675    use crate::custom_family::{BlockwiseFitOptions, ExactNewtonJointHessianWorkspace};
3676    // Deliberately NOT importing `configure_global_policy`: the process-wide
3677    // policy is a first-writer-wins `OnceLock`, so a test that writes it decides
3678    // the backend every other test in this binary selects. `GpuPolicy` itself is
3679    // no longer named here either — the availability probe moved behind
3680    // `gam_gpu::test_gate::gpu_for_test`, which owns the explicit `Auto`.
3681    use ndarray::{Array1, Array2};
3682    use std::hint::black_box;
3683    use std::sync::atomic::AtomicUsize;
3684    use std::time::{Duration, Instant};
3685
3686    /// Assert the dispatch-worthiness claim and record the timings without
3687    /// asserting on them (#2487, SPEC rule 19).
3688    ///
3689    /// These gates asserted `cpu_median / gpu_median >= 2.0` (walked down from
3690    /// 5× and 10× calibration-box ratios). A ratio of two wall-clock medians is
3691    /// a property of whoever else is on the box, not of the kernel: under
3692    /// co-tenancy the device arm degrades harder than the host arm, so the
3693    /// ratio collapses toward 1 exactly when the fleet is busiest, and the red
3694    /// gets read as a code regression. The claim these gates exist to make —
3695    /// "this shape belongs on the device" — is decided instead by the
3696    /// calibrated policy, whose row crossover is a per-device *measurement*.
3697    ///
3698    /// The medians stay in the log as the hill-climbing perf record, which is
3699    /// what a timing is good for.
3700    #[cfg(target_os = "linux")]
3701    fn assert_row_batch_dispatch_worthy(
3702        label: &str,
3703        policy: &gam_gpu::policy::GpuDispatchPolicy,
3704        n: usize,
3705    ) {
3706        assert!(
3707            policy.row_batch_target_is_gpu(n),
3708            "{label}: n={n} rows is below this device's calibrated row-kernel \
3709             crossover ({}), so the fixture no longer exercises a shape the \
3710             dispatch policy would send to the device — grow the fixture rather \
3711             than lowering the crossover",
3712            policy.row_kernel_min_n
3713        );
3714        assert!(
3715            !policy.row_batch_target_is_gpu(0),
3716            "{label}: the dispatch predicate admitted an empty batch, so the \
3717             assertion above proves nothing about n={n}"
3718        );
3719    }
3720
3721    /// #2422 device-free half, run by [`cuda_runtime_for_test`] on every host
3722    /// that has no CUDA device.
3723    ///
3724    /// Every CUDA-gated test in this module early-returns when the runtime is
3725    /// absent, and that return happens before the test's first assertion — so
3726    /// the test reports `passed` having executed nothing. Because every CI
3727    /// runner is device-free, that is the state this entire module has been in.
3728    ///
3729    /// The contract that IS checkable without a device is the production
3730    /// entry's refusal. `launch_bms_flex_row_kernel` validates its inputs and
3731    /// then reaches `launch_linux`, which opens with `RowKernelBackend::probe()?`;
3732    /// with no device that probe fails, so the entry must return `Err` carrying
3733    /// the device-absence reason. An entry that returns `Ok` on a device-free
3734    /// host has fabricated device state — the #1551 silent-fallback class, and
3735    /// exactly what a `return` before the first assertion could never see.
3736    fn assert_row_kernel_seam_declines_without_cuda() {
3737        let buffers = make_buffers(1, 4, 1, 1);
3738        // The fixture must be one the entry ACCEPTS. `launch_bms_flex_row_kernel`
3739        // rejects malformed inputs with `Err` as well, so an invalid fixture
3740        // would satisfy the refusal check below without the device seam ever
3741        // being reached — the assertion would hold for the wrong reason.
3742        minimal_inputs(&buffers)
3743            .validate()
3744            .expect("the device-free half must present inputs the row-kernel entry accepts");
3745
3746        match launch_bms_flex_row_kernel(minimal_inputs(&buffers)) {
3747            Ok(_) => panic!(
3748                "no CUDA runtime on this host, yet the BMS FLEX row-kernel entry returned Ok \
3749                 — the seam fabricated device state (#1551 class)"
3750            ),
3751            Err(GpuError::DriverCallFailed { reason }) if reason.contains("s_f") => panic!(
3752                "the row-kernel entry refused over its INPUTS rather than the absent device, \
3753                 so this proves nothing about the device seam: {reason}"
3754            ),
3755            Err(_) => {}
3756        }
3757    }
3758
3759    /// Resolve the CUDA runtime for a device-gated test.
3760    ///
3761    /// `Err` is a real driver fault and fails loudly — it must never be
3762    /// confused with device absence. `None` means the host genuinely has no
3763    /// device, and before handing that back this asserts the device-free half
3764    /// of the caller's contract, so a caller that early-returns on `None` still
3765    /// proves something real instead of reporting `passed` with zero assertions
3766    /// executed (#2422). Curing it here rather than at each call site is
3767    /// deliberate: every CUDA-gated test in this module routes through this one
3768    /// function, and a fix applied per-site is a fix that misses the next site
3769    /// the moment one is written.
3770    ///
3771    /// The availability question goes through `gam_gpu::test_gate::gpu_for_test`,
3772    /// which probes with an EXPLICIT `GpuPolicy::Auto` and never writes the
3773    /// process-wide policy — that policy is a first-writer-wins `OnceLock`
3774    /// shared with every other test in this binary. Routing through the shared
3775    /// gate adds two things this helper did not have: the skip is COUNTED, so a
3776    /// suite can assert how many gated tests declined, and a
3777    /// `GpuPolicy::Required` lane turns an absent device into a failure without
3778    /// any per-test opt-in. The seam assertion below is this module's own and is
3779    /// stronger than the gate's; it is kept.
3780    fn cuda_runtime_for_test(
3781        test_name: &str,
3782    ) -> Option<&'static gam_gpu::device_runtime::GpuRuntime> {
3783        let skips_before = gam_gpu::test_gate::skipped_for_absent_device();
3784        match gam_gpu::test_gate::gpu_for_test(test_name) {
3785            gam_gpu::test_gate::GpuTestGate::Ready(runtime) => Some(runtime),
3786            gam_gpu::test_gate::GpuTestGate::AbsentDevice => {
3787                gam_gpu::test_gate::assert_absent_device_was_counted(skips_before);
3788                eprintln!(
3789                    "[{test_name}] no CUDA device — asserting the device-free seam contract \
3790                     instead of skipping"
3791                );
3792                assert_row_kernel_seam_declines_without_cuda();
3793                None
3794            }
3795        }
3796    }
3797
3798    fn assert_array1_close_932(label: &str, expected: &Array1<f64>, actual: &Array1<f64>) {
3799        assert_eq!(expected.len(), actual.len(), "{label}: length mismatch");
3800        for (index, (&want, &got)) in expected.iter().zip(actual).enumerate() {
3801            let tolerance = 2.0e-8 * (1.0 + want.abs());
3802            assert!(
3803                want.is_finite() && got.is_finite() && (want - got).abs() <= tolerance,
3804                "{label}[{index}]: expected={want:.17e} actual={got:.17e} tolerance={tolerance:.3e}"
3805            );
3806        }
3807    }
3808
3809    /// Mandatory A100 acceptance hook: once a device is present, every probe,
3810    /// upload, launch, synchronization, status, or download failure aborts the
3811    /// test instead of turning into a skip. The device arm is entered through
3812    /// [`cuda_runtime_for_test`], which asserts the device-free seam contract
3813    /// before it hands back `None`, so the host arm still proves something.
3814    ///
3815    /// It does NOT claim `gam_gpu::GpuPolicy::Required`. That policy lives in a
3816    /// process-wide `OnceLock` with first-writer-wins semantics, so a test that
3817    /// sets it changes the backend selection every OTHER test in the same binary
3818    /// makes: `resolve(Required)` on a device-free host is `Err`, and the ~90
3819    /// unrelated tests that reach `resolve(global_policy())` then fail on the
3820    /// device-absence error. Whether they do depends on whether this test or a
3821    /// production `configure_global_policy(Auto)` claimed the slot first, which
3822    /// is scheduling-dependent. Required also buys nothing here: the cache-
3823    /// residency decision is `resolve(policy)?.is_some()`, identical under Auto
3824    /// on a host that actually has a device.
3825    #[test]
3826    fn mandatory_required_gpu_workspace_consumes_device_cache_end_to_end_932() {
3827        if cuda_runtime_for_test("mandatory_required_gpu_workspace_consumes_device_cache_end_to_end_932")
3828            .is_none()
3829        {
3830            return;
3831        }
3832
3833        let (family, states) = row_kernel_tests::parity_415::make_flex_parity_family(256, 8, 6);
3834        let mut workspace = BernoulliMarginalSlopeExactNewtonJointHessianWorkspace::new(
3835            family,
3836            states,
3837            BlockwiseFitOptions::default(),
3838        )
3839        .expect("#932 device workspace must build its device row cache");
3840
3841        assert!(
3842            matches!(
3843                &workspace.cache.row_primary_hessians,
3844                RowPrimaryEvalCache::Device(_)
3845            ),
3846            "a full-FLEX workspace built on a device-present host must retain \
3847             RowPrimaryEvalCache::Device"
3848        );
3849        {
3850            let device = workspace
3851                .cache
3852                .row_primary_hessians
3853                .device()
3854                .expect("device cache variant");
3855            assert!(
3856                device
3857                    .primary
3858                    .h
3859                    .as_ref()
3860                    .is_some_and(|range| !range.is_empty())
3861                    && device
3862                        .primary
3863                        .w
3864                        .as_ref()
3865                        .is_some_and(|range| !range.is_empty()),
3866                "mandatory fixture must carry active h and w primary blocks"
3867            );
3868            assert!(
3869                device
3870                    .block
3871                    .h
3872                    .as_ref()
3873                    .is_some_and(|range| !range.is_empty())
3874                    && device
3875                        .block
3876                        .w
3877                        .as_ref()
3878                        .is_some_and(|range| !range.is_empty()),
3879                "mandatory fixture must carry active h and w coefficient blocks"
3880            );
3881        }
3882        for operation in ["host HVP replay", "host diagonal replay"] {
3883            let error = workspace
3884                .cache
3885                .row_primary_hessians
3886                .reject_device_cpu_recompute(operation)
3887                .expect_err("a selected device cache must reject host row recomputation");
3888            assert!(
3889                error.contains("device-resident row evaluation selected")
3890                    && error.contains("CPU row recomputation is forbidden"),
3891                "unexpected fail-closed diagnostic: {error}"
3892            );
3893        }
3894
3895        let total = workspace.cache.slices.total;
3896        let direction = Array1::from_shape_fn(total, |index| {
3897            let sign = if index % 2 == 0 { 1.0 } else { -1.0 };
3898            sign * (0.025 + 0.0075 * index as f64)
3899        });
3900        let joint_ll = workspace
3901            .joint_log_likelihood_evaluation()
3902            .expect("device joint log-likelihood")
3903            .expect("device joint log-likelihood must be present");
3904        let joint = workspace
3905            .joint_gradient_evaluation()
3906            .expect("device joint gradient")
3907            .expect("device joint gradient must be present");
3908        assert!(joint_ll.is_finite());
3909        assert_eq!(joint.log_likelihood.to_bits(), joint_ll.to_bits());
3910        assert_eq!(joint.gradient.len(), total);
3911        assert!(joint.gradient.iter().all(|value| value.is_finite()));
3912
3913        let hvp = workspace
3914            .hessian_matvec(&direction)
3915            .expect("device HVP")
3916            .expect("device HVP must be present");
3917        let mut hvp_into = Array1::from_elem(total, f64::NAN);
3918        assert!(
3919            workspace
3920                .hessian_matvec_into(&direction, &mut hvp_into)
3921                .expect("device HVP-into"),
3922            "device HVP-into must report that it handled the direction"
3923        );
3924        assert_array1_close_932("HVP owned/into", &hvp, &hvp_into);
3925
3926        let rhs = Array2::from_shape_fn((total, 3), |(row, column)| {
3927            (row as f64 + 1.0)
3928                * (column as f64 + 0.5)
3929                * 0.011
3930                * if (row + column) % 3 == 0 { -1.0 } else { 1.0 }
3931        });
3932        let mut applied = Array2::<f64>::from_elem((total, rhs.ncols()), f64::NAN);
3933        assert!(
3934            workspace
3935                .hessian_apply_mat(&rhs, &mut applied)
3936                .expect("device multi-RHS apply"),
3937            "device multi-RHS apply must report that it handled the matrix"
3938        );
3939        let diagonal = workspace
3940            .hessian_diagonal()
3941            .expect("device diagonal")
3942            .expect("device diagonal must be present");
3943        let dense = workspace
3944            .hessian_dense_forced()
3945            .expect("device forced dense Hessian")
3946            .expect("device forced dense Hessian must be present");
3947        assert_eq!(dense.dim(), (total, total));
3948        assert_array1_close_932("dense * v / HVP", &dense.dot(&direction), &hvp);
3949        assert_array1_close_932("dense diagonal", &dense.diag().to_owned(), &diagonal);
3950        let dense_applied = dense.dot(&rhs);
3951        for column in 0..rhs.ncols() {
3952            assert_array1_close_932(
3953                &format!("dense * V / apply_mat column {column}"),
3954                &dense_applied.column(column).to_owned(),
3955                &applied.column(column).to_owned(),
3956            );
3957        }
3958
3959        // The resident cache is the numerical authority. Poisoning every host
3960        // block-state number after construction must not alter HVP/diagonal;
3961        // any accidental host replay would either propagate NaNs or error.
3962        for state in &mut workspace.block_states {
3963            state.beta.fill(f64::NAN);
3964            state.eta.fill(f64::NAN);
3965        }
3966        let poisoned_hvp = workspace
3967            .hessian_matvec(&direction)
3968            .expect("device HVP after host-state poison")
3969            .expect("device HVP after host-state poison must be present");
3970        let poisoned_diagonal = workspace
3971            .hessian_diagonal()
3972            .expect("device diagonal after host-state poison")
3973            .expect("device diagonal after host-state poison must be present");
3974        assert_eq!(
3975            hvp.as_slice(),
3976            poisoned_hvp.as_slice(),
3977            "fixed-order device HVP changed after poisoning host block state"
3978        );
3979        assert_eq!(
3980            diagonal.as_slice(),
3981            poisoned_diagonal.as_slice(),
3982            "fixed-order device diagonal changed after poisoning host block state"
3983        );
3984    }
3985
3986    /// Temporary #932 release-only evidence hook. It times the strongest
3987    /// production CPU row batch (the Rayon `build_row_primary_hessian_pin`)
3988    /// against the complete generated GPU row path: host packing, device
3989    /// moment production, every transfer/allocation, row launch, synchronize,
3990    /// and status download. Run this exact test in a fresh release process so
3991    /// `cold_gpu_e2e_nvrtc_ms` includes the first NVRTC loads and the 21 ABBA
3992    /// samples describe only the subsequently cached compiler state.
3993    #[test]
3994    fn release_measure_generated_bms_full_row_vs_strongest_cpu_932() {
3995        const N: usize = 32_768;
3996        const WARMUPS: usize = 3;
3997        const SAMPLES: usize = 21;
3998
3999        if cuda_runtime_for_test("release_measure_generated_bms_full_row_vs_strongest_cpu_932")
4000            .is_none()
4001        {
4002            return;
4003        }
4004
4005        // Cubic deviation runtimes expose `num_internal_knots + 1` live
4006        // controls since the #2319 knot-selection orbit canonicalization, so
4007        // 9/7 internal knots give p_h=10, p_w=8 — the same r=20 measurement
4008        // shape this cell has always timed.
4009        let (family, states) = row_kernel_tests::parity_415::make_flex_parity_family(N, 9, 7);
4010        let cache = family
4011            .build_exact_eval_cache(&states)
4012            .expect("full-row timing exact cache");
4013        let r = cache.primary.total;
4014        assert_eq!(r, 20, "9/7 knot fixture must expose primary width r=20");
4015        let marginal = family
4016            .marginal_design
4017            .as_dense_ref()
4018            .expect("timing fixture marginal design must be dense");
4019        let logslope = family
4020            .logslope_design
4021            .as_dense_ref()
4022            .expect("timing fixture logslope design must be dense");
4023        assert!(marginal.is_standard_layout() && logslope.is_standard_layout());
4024        let marginal_slice = marginal
4025            .as_slice()
4026            .expect("timing fixture marginal design is contiguous");
4027        let logslope_slice = logslope
4028            .as_slice()
4029            .expect("timing fixture logslope design is contiguous");
4030        let block = BmsFlexBlockLayout {
4031            p_m: cache.slices.marginal.len(),
4032            p_g: cache.slices.logslope.len(),
4033            h: cache.slices.h.clone(),
4034            w: cache.slices.w.clone(),
4035            p_total: cache.slices.total,
4036        };
4037        let primary = BmsFlexPrimaryLayout {
4038            h: cache.primary.h.clone(),
4039            w: cache.primary.w.clone(),
4040            r,
4041        };
4042        assert!(
4043            primary.h.as_ref().is_some_and(|range| !range.is_empty())
4044                && primary.w.as_ref().is_some_and(|range| !range.is_empty()),
4045            "full-row timing fixture must exercise both h and w"
4046        );
4047        let pin_bytes =
4048            crate::bms::family::BernoulliMarginalSlopeFamily::row_primary_eval_tile_bytes(N, r);
4049
4050        let run_cpu = || {
4051            let completed = AtomicUsize::new(0);
4052            family
4053                .build_row_primary_hessian_pin(
4054                    &states,
4055                    &cache,
4056                    0..N,
4057                    &completed,
4058                    N.saturating_add(1),
4059                    Instant::now(),
4060                    pin_bytes,
4061                )
4062                .expect("production Rayon row-primary batch")
4063        };
4064        let run_gpu = || {
4065            let owned = family
4066                .pack_bms_flex_row_kernel_inputs(&states, &cache)
4067                .expect("production BMS GPU packing")
4068                .expect("StandardNormal full-FLEX timing fixture must pack");
4069            launch_bms_flex_row_kernel_device_resident(
4070                owned.as_borrowed(),
4071                marginal_slice,
4072                logslope_slice,
4073                block.clone(),
4074                primary.clone(),
4075            )
4076            .expect("production device-resident row launch")
4077        };
4078        let measure_cpu = || {
4079            let started = Instant::now();
4080            let output = black_box(run_cpu());
4081            (started.elapsed(), output)
4082        };
4083        let measure_gpu = || {
4084            let started = Instant::now();
4085            let output = black_box(run_gpu());
4086            (started.elapsed(), output)
4087        };
4088
4089        let cold_started = Instant::now();
4090        let cold_gpu = black_box(run_gpu());
4091        let cold_gpu_e2e_nvrtc = cold_started.elapsed();
4092        drop(cold_gpu);
4093        for _ in 0..WARMUPS {
4094            black_box(run_cpu());
4095            black_box(run_gpu());
4096        }
4097
4098        let mut cpu_samples = Vec::<Duration>::with_capacity(SAMPLES);
4099        let mut gpu_samples = Vec::<Duration>::with_capacity(SAMPLES);
4100        let mut last_cpu = None;
4101        let mut last_gpu = None;
4102        for sample in 0..SAMPLES {
4103            // Alternating AB/BA pairs yield the repeating ABBA ordering and
4104            // cancel monotone thermal/frequency drift without averaging away
4105            // an individually slow cell.
4106            if sample % 2 == 0 {
4107                let (cpu_elapsed, cpu) = measure_cpu();
4108                cpu_samples.push(cpu_elapsed);
4109                let (gpu_elapsed, gpu) = measure_gpu();
4110                gpu_samples.push(gpu_elapsed);
4111                if sample + 1 == SAMPLES {
4112                    last_cpu = Some(cpu);
4113                    last_gpu = Some(gpu);
4114                }
4115            } else {
4116                let (gpu_elapsed, gpu) = measure_gpu();
4117                gpu_samples.push(gpu_elapsed);
4118                let (cpu_elapsed, cpu) = measure_cpu();
4119                cpu_samples.push(cpu_elapsed);
4120                drop(gpu);
4121                drop(cpu);
4122            }
4123        }
4124        let cpu = last_cpu.expect("final CPU sample retained for parity");
4125        let gpu = last_gpu.expect("final GPU sample retained for parity");
4126
4127        let stream = HvpKernelBackend::probe()
4128            .expect("HVP backend remains available")
4129            .stream
4130            .clone();
4131        let gpu_neglog = stream
4132            .clone_dtoh(&gpu.neglog)
4133            .expect("download timed GPU neglog for parity");
4134        let gpu_grad = stream
4135            .clone_dtoh(&gpu.grad)
4136            .expect("download timed GPU gradient for parity");
4137        let gpu_hess = stream
4138            .clone_dtoh(&gpu.hess)
4139            .expect("download timed GPU Hessian for parity");
4140        let cpu_channels = [
4141            cpu.neglog().as_slice().expect("CPU neglog is contiguous"),
4142            cpu.grad().as_slice().expect("CPU gradient is contiguous"),
4143            cpu.hess().as_slice().expect("CPU Hessian is contiguous"),
4144        ];
4145        let gpu_channels = [
4146            gpu_neglog.as_slice(),
4147            gpu_grad.as_slice(),
4148            gpu_hess.as_slice(),
4149        ];
4150        let mut nonfinite = 0_usize;
4151        let mut max_abs = 0.0_f64;
4152        let mut max_scaled = 0.0_f64;
4153        let mut cpu_digest = 0.0_f64;
4154        let mut gpu_digest = 0.0_f64;
4155        let mut digest_index = 0_usize;
4156        for (cpu_channel, gpu_channel) in cpu_channels.iter().zip(gpu_channels) {
4157            assert_eq!(cpu_channel.len(), gpu_channel.len());
4158            for (&host, &device) in cpu_channel.iter().zip(gpu_channel) {
4159                if !host.is_finite() || !device.is_finite() {
4160                    nonfinite += 1;
4161                }
4162                let difference = (host - device).abs();
4163                let tolerance = 1.0e-8 * (1.0 + host.abs());
4164                max_abs = max_abs.max(difference);
4165                max_scaled = max_scaled.max(difference / tolerance);
4166                let weight = 1.0 + (digest_index % 251) as f64 / 251.0;
4167                cpu_digest += weight * host;
4168                gpu_digest += weight * device;
4169                digest_index += 1;
4170            }
4171        }
4172        assert_eq!(
4173            nonfinite, 0,
4174            "full-row CPU/GPU output contains non-finite values"
4175        );
4176        assert!(
4177            max_scaled <= 1.0,
4178            "full-row CPU/GPU parity exceeded tolerance: max_abs={max_abs:.3e} max_scaled={max_scaled:.3e}"
4179        );
4180
4181        let mut cpu_ms = cpu_samples
4182            .iter()
4183            .map(|sample| sample.as_secs_f64() * 1.0e3)
4184            .collect::<Vec<_>>();
4185        let mut gpu_ms = gpu_samples
4186            .iter()
4187            .map(|sample| sample.as_secs_f64() * 1.0e3)
4188            .collect::<Vec<_>>();
4189        cpu_ms.sort_by(f64::total_cmp);
4190        gpu_ms.sort_by(f64::total_cmp);
4191        let p25 = SAMPLES / 4;
4192        let p50 = SAMPLES / 2;
4193        let p75 = 3 * SAMPLES / 4;
4194        let conservative_speedup = cpu_ms[p25] / gpu_ms[p75];
4195        let median_speedup = cpu_ms[p50] / gpu_ms[p50];
4196        let cpu_distribution = cpu_ms
4197            .iter()
4198            .map(|value| format!("{value:.6}"))
4199            .collect::<Vec<_>>()
4200            .join(",");
4201        let gpu_distribution = gpu_ms
4202            .iter()
4203            .map(|value| format!("{value:.6}"))
4204            .collect::<Vec<_>>()
4205            .join(",");
4206        println!(
4207            "G932_BMS_FULL_ROW n={N} r={r} warmups={WARMUPS} samples={SAMPLES} \
4208             cold_gpu_e2e_nvrtc_ms={:.6} cpu_ms_p25={:.6} cpu_ms_p50={:.6} cpu_ms_p75={:.6} \
4209             gpu_ms_p25={:.6} gpu_ms_p50={:.6} gpu_ms_p75={:.6} \
4210             speedup_conservative_cpu_p25_over_gpu_p75={conservative_speedup:.6} \
4211             speedup_median={median_speedup:.6} parity_max_abs={max_abs:.9e} \
4212             parity_max_scaled={max_scaled:.9e} cpu_digest={cpu_digest:.17e} \
4213             gpu_digest={gpu_digest:.17e} nonfinite={nonfinite} \
4214             cpu_ms_sorted=[{cpu_distribution}] gpu_ms_sorted=[{gpu_distribution}]",
4215            cold_gpu_e2e_nvrtc.as_secs_f64() * 1.0e3,
4216            cpu_ms[p25],
4217            cpu_ms[p50],
4218            cpu_ms[p75],
4219            gpu_ms[p25],
4220            gpu_ms[p50],
4221            gpu_ms[p75],
4222        );
4223    }
4224
4225    #[test]
4226    fn dense_hvp_batches_transpose_column_images_in_bounded_groups_932() {
4227        let p_total = 2 * BMS_FLEX_ROW_HVP_MAX_RHS + 3;
4228        let matrix = (0..p_total * p_total)
4229            .map(|index| {
4230                let row = index / p_total;
4231                let column = index % p_total;
4232                1000.0 * row as f64 + column as f64 + 0.25
4233            })
4234            .collect::<Vec<_>>();
4235        let mut observed_batch_sizes = Vec::new();
4236        let dense = materialize_dense_from_hvp_batches(p_total, |basis, rhs_count| {
4237            observed_batch_sizes.push(rhs_count);
4238            let mut images = vec![0.0_f64; rhs_count * p_total];
4239            for rhs in 0..rhs_count {
4240                for row in 0..p_total {
4241                    images[rhs * p_total + row] = (0..p_total)
4242                        .map(|column| {
4243                            matrix[row * p_total + column] * basis[rhs * p_total + column]
4244                        })
4245                        .sum();
4246                }
4247            }
4248            Ok(images)
4249        })
4250        .expect("synthetic H*I batches must materialize");
4251        assert_eq!(dense, matrix);
4252        assert_eq!(
4253            observed_batch_sizes,
4254            vec![BMS_FLEX_ROW_HVP_MAX_RHS, BMS_FLEX_ROW_HVP_MAX_RHS, 3]
4255        );
4256    }
4257
4258    pub(crate) fn minimal_inputs<'a>(buffers: &'a TestBuffers) -> BmsFlexRowKernelInputs<'a> {
4259        BmsFlexRowKernelInputs {
4260            n_rows: 1,
4261            r: 4,
4262            p_h: 1,
4263            p_w: 1,
4264            q: &buffers.q,
4265            b: &buffers.b,
4266            mu_1: &buffers.mu_1,
4267            mu_2: &buffers.mu_2,
4268            z_obs: &buffers.z_obs,
4269            y: &buffers.y,
4270            w: &buffers.w,
4271            e_obs: &buffers.e_obs,
4272            s_f: 1.0,
4273            cell_offsets: &buffers.cell_offsets,
4274            cell_c0: &buffers.cell_c0,
4275            cell_c1: &buffers.cell_c1,
4276            cell_c2: &buffers.cell_c2,
4277            cell_c3: &buffers.cell_c3,
4278            cell_a: &buffers.cell_a,
4279            cell_aa: &buffers.cell_aa,
4280            cell_r: &buffers.cell_r,
4281            cell_ar: &buffers.cell_ar,
4282            cell_sbb: &buffers.cell_sbb,
4283            cell_sbh: &buffers.cell_sbh,
4284            cell_sbw: &buffers.cell_sbw,
4285            cell_moments: CellMomentsSource::Host(&buffers.cell_moments),
4286            chi_obs: &buffers.chi_obs,
4287            xi_obs: &buffers.xi_obs,
4288            rho_u: &buffers.rho_u,
4289            tau_u: &buffers.tau_u,
4290            r_uv: &buffers.r_uv,
4291        }
4292    }
4293
4294    pub(crate) struct TestBuffers {
4295        pub(crate) q: Vec<f64>,
4296        pub(crate) b: Vec<f64>,
4297        pub(crate) mu_1: Vec<f64>,
4298        pub(crate) mu_2: Vec<f64>,
4299        pub(crate) z_obs: Vec<f64>,
4300        pub(crate) y: Vec<f64>,
4301        pub(crate) w: Vec<f64>,
4302        pub(crate) e_obs: Vec<f64>,
4303        pub(crate) cell_offsets: Vec<u32>,
4304        pub(crate) cell_c0: Vec<f64>,
4305        pub(crate) cell_c1: Vec<f64>,
4306        pub(crate) cell_c2: Vec<f64>,
4307        pub(crate) cell_c3: Vec<f64>,
4308        pub(crate) cell_a: Vec<f64>,
4309        pub(crate) cell_aa: Vec<f64>,
4310        pub(crate) cell_r: Vec<f64>,
4311        pub(crate) cell_ar: Vec<f64>,
4312        pub(crate) cell_sbb: Vec<f64>,
4313        pub(crate) cell_sbh: Vec<f64>,
4314        pub(crate) cell_sbw: Vec<f64>,
4315        pub(crate) cell_moments: Vec<f64>,
4316        pub(crate) chi_obs: Vec<f64>,
4317        pub(crate) xi_obs: Vec<f64>,
4318        pub(crate) rho_u: Vec<f64>,
4319        pub(crate) tau_u: Vec<f64>,
4320        pub(crate) r_uv: Vec<f64>,
4321    }
4322
4323    pub(crate) fn make_buffers(n_cells: u32, r: usize, p_h: usize, p_w: usize) -> TestBuffers {
4324        let cells = n_cells as usize;
4325        TestBuffers {
4326            q: vec![0.1; 1],
4327            b: vec![0.5; 1],
4328            mu_1: vec![0.3; 1],
4329            mu_2: vec![0.07; 1],
4330            z_obs: vec![0.0; 1],
4331            y: vec![1.0; 1],
4332            w: vec![1.0; 1],
4333            e_obs: vec![0.15; 1],
4334            cell_offsets: vec![0, n_cells],
4335            cell_c0: vec![0.2; cells],
4336            cell_c1: vec![-0.1; cells],
4337            cell_c2: vec![0.05; cells],
4338            cell_c3: vec![-0.02; cells],
4339            cell_a: vec![0.1; cells * 4],
4340            cell_aa: vec![0.0; cells * 4],
4341            cell_r: vec![0.05; cells * (r - 1) * 4],
4342            cell_ar: vec![0.0; cells * (r - 1) * 4],
4343            cell_sbb: vec![0.0; cells * 4],
4344            cell_sbh: vec![0.0; cells * p_h * 4],
4345            cell_sbw: vec![0.0; cells * p_w * 4],
4346            cell_moments: vec![1.0; cells * MOMENT_STRIDE],
4347            chi_obs: vec![1.0; 1],
4348            xi_obs: vec![0.0; 1],
4349            rho_u: vec![0.0; r],
4350            tau_u: vec![0.0; r],
4351            r_uv: vec![0.0; r * r],
4352        }
4353    }
4354
4355    #[test]
4356    pub(crate) fn validate_accepts_minimal_inputs() {
4357        let buffers = make_buffers(2, 4, 1, 1);
4358        let inputs = minimal_inputs(&buffers);
4359        assert!(inputs.validate().is_ok());
4360    }
4361
4362    #[test]
4363    pub(crate) fn validate_accepts_r33_with_active_h_and_w_blocks() {
4364        let r = 33;
4365        let p_h = 16;
4366        let p_w = 15;
4367        let buffers = make_buffers(1, r, p_h, p_w);
4368        let inputs = BmsFlexRowKernelInputs {
4369            r,
4370            p_h,
4371            p_w,
4372            rho_u: &buffers.rho_u,
4373            tau_u: &buffers.tau_u,
4374            r_uv: &buffers.r_uv,
4375            cell_r: &buffers.cell_r,
4376            cell_ar: &buffers.cell_ar,
4377            cell_sbh: &buffers.cell_sbh,
4378            cell_sbw: &buffers.cell_sbw,
4379            ..minimal_inputs(&buffers)
4380        };
4381        inputs
4382            .validate()
4383            .expect("r=33 is a valid checked shape, not a semantic width boundary");
4384    }
4385
4386    #[test]
4387    pub(crate) fn checked_shape_len_rejects_arithmetic_overflow() {
4388        let err = checked_shape_len("overflow test", &[usize::MAX, 2])
4389            .expect_err("shape multiplication must fail closed");
4390        assert!(err.to_string().contains("shape product overflow"));
4391    }
4392
4393    #[test]
4394    pub(crate) fn validate_rejects_zero_rows_before_cuda_grid_construction() {
4395        let buffers = make_buffers(1, 4, 1, 1);
4396        let inputs = BmsFlexRowKernelInputs {
4397            n_rows: 0,
4398            ..minimal_inputs(&buffers)
4399        };
4400        let err = inputs
4401            .validate()
4402            .expect_err("zero-row launch must fail closed");
4403        assert!(err.to_string().contains("n_rows must be > 0"));
4404    }
4405
4406    #[test]
4407    pub(crate) fn validate_rejects_mismatched_r_decomposition() {
4408        let buffers = make_buffers(1, 4, 1, 1);
4409        let bad_inputs = BmsFlexRowKernelInputs {
4410            r: 4,
4411            p_h: 1,
4412            p_w: 2, // inconsistent with r = 4
4413            ..minimal_inputs(&buffers)
4414        };
4415        let err = bad_inputs
4416            .validate()
4417            .expect_err("inconsistent r vs p_h+p_w must fail");
4418        let msg = err.to_string();
4419        assert!(msg.contains("p_h"), "got: {msg}");
4420        assert!(msg.contains("p_w"), "got: {msg}");
4421    }
4422
4423    #[test]
4424    pub(crate) fn validate_rejects_non_monotone_offsets() {
4425        // `minimal_inputs` hard-codes `n_rows = 1`, so the CSR-style row
4426        // pointer length is `n + 1 = 2`. Pin both `offsets[1] = total_cells`
4427        // and `cell_c0.len() = total_cells = 2` from `make_buffers(2, …)`,
4428        // then violate monotonicity by setting `offsets[0] > offsets[1]`;
4429        // every length / per-cell-count check is satisfied so the only
4430        // failure mode left is the monotonicity guard.
4431        let mut buffers = make_buffers(2, 4, 1, 1);
4432        buffers.cell_offsets = vec![5, 2];
4433        let inputs = minimal_inputs(&buffers);
4434        let err = inputs
4435            .validate()
4436            .expect_err("non-monotone offsets must fail");
4437        let msg = err.to_string();
4438        assert!(msg.contains("monotone"), "got: {msg}");
4439    }
4440
4441    #[test]
4442    pub(crate) fn validate_rejects_mismatched_cell_moments_length() {
4443        let mut buffers = make_buffers(2, 4, 1, 1);
4444        buffers.cell_moments.pop(); // length now 2*10 - 1
4445        let inputs = minimal_inputs(&buffers);
4446        let err = inputs.validate().expect_err("short cell_moments must fail");
4447        let msg = err.to_string();
4448        assert!(msg.contains("cell_moments"), "got: {msg}");
4449    }
4450
4451    #[test]
4452    pub(crate) fn launch_on_non_linux_reports_driver_library_unavailable() {
4453        // Mac/Windows builds must surface a typed `DriverLibraryUnavailable`
4454        // rather than panicking or returning Ok. On Linux this test is
4455        // skipped because the kernel actually launches.
4456        #[cfg(target_os = "linux")]
4457        {
4458            if cuda_runtime_for_test("bms_flex_row launch smoke test").is_none() {
4459                return;
4460            }
4461            let buffers = make_buffers(1, 4, 1, 1);
4462            let inputs = minimal_inputs(&buffers);
4463            launch_bms_flex_row_kernel(inputs)
4464                .expect("BMS FLEX row kernel must launch after CUDA admission");
4465        }
4466        #[cfg(not(target_os = "linux"))]
4467        {
4468            let buffers = make_buffers(1, 4, 1, 1);
4469            let inputs = minimal_inputs(&buffers);
4470            match launch_bms_flex_row_kernel(inputs) {
4471                Err(GpuError::DriverLibraryUnavailable { reason }) => {
4472                    assert!(
4473                        reason.contains("Linux-only"),
4474                        "expected Linux-only hint, got: {reason}"
4475                    );
4476                }
4477                other => panic!("expected DriverLibraryUnavailable on non-Linux, got {other:?}"),
4478            }
4479        }
4480    }
4481
4482    #[test]
4483    pub(crate) fn s_f_must_be_positive_and_finite() {
4484        let buffers = make_buffers(1, 4, 1, 1);
4485        let mut inputs = minimal_inputs(&buffers);
4486        inputs.s_f = 0.0;
4487        match launch_bms_flex_row_kernel(inputs) {
4488            Err(GpuError::DriverCallFailed { reason }) => {
4489                assert!(reason.contains("s_f"), "got: {reason}");
4490            }
4491            other => panic!("expected DriverCallFailed for s_f=0, got {other:?}"),
4492        }
4493    }
4494
4495    /// Independent finite-difference correctness lock on the device probit
4496    /// Mills layer — the most optimizer-sensitive, drift-prone term in the
4497    /// whole row kernel (issue #415: "third/fourth-order derivative
4498    /// contractions drift silently … formulas are complex and
4499    /// optimizer-sensitive"). The generated device kernel closes with this
4500    /// Mills algebra:
4501    ///
4502    /// ```text
4503    ///     m       = s · e_obs ;  s = 2y − 1
4504    ///     A       = −w · s · λ(m)
4505    ///     B       =  w · λ(m) · (m + λ(m))
4506    ///     neglog  = −w · log Φ(s · e_obs)
4507    ///     g_u     = A · bar_e_u
4508    ///     H_uv    = B · bar_e_u · bar_e_v + A · bar_e_uv
4509    /// ```
4510    ///
4511    /// Holding the observed derivative jets `bar_e_u`/`bar_e_uv` fixed, the
4512    /// row neglog is a function of the observed predictor VALUE `e := e_obs`,
4513    /// not of the q-axis first derivative `bar_e_u[0]`; by the assembled
4514    /// formula `∂neglog/∂e = A` and `∂²neglog/∂e² = B`. This test reconstructs
4515    /// `A`, `B`, and `neglog` through the canonical host numerics, then verifies the
4516    /// analytic `A`/`B` against high-order central differences of
4517    /// `e ↦ −w · log Φ(s·e)`. A drift in the kernel's Mills derivatives —
4518    /// fails independently of the CPU↔CUDA production parity check. Bounds are
4519    /// the genuine fifth-order central-difference
4520    /// truncation floor; they are not weakened to pass.
4521    #[test]
4522    pub(crate) fn device_mills_layer_matches_finite_differences() {
4523        // Probit neglog as a function of the
4524        // observed scalar predictor `e` with weight `w` and label `y`.
4525        let neglog_of = |e: f64, y: f64, w: f64| -> f64 {
4526            let s = 2.0 * y - 1.0;
4527            let (log_cdf, _) = host_log_ndtr_and_mills(s * e);
4528            -w * log_cdf
4529        };
4530        // Analytic first/second derivatives wrt `e` — the exact `A`/`B` the
4531        // kernel writes into `grad`/`hess`.
4532        let ab_of = |e: f64, y: f64, w: f64| -> (f64, f64) {
4533            let s = 2.0 * y - 1.0;
4534            let m_arg = s * e;
4535            let (_, lambda, probit_curvature) = host_log_ndtr_mills_curvature(m_arg);
4536            let a_i = -w * s * lambda;
4537            let b_i = w * probit_curvature;
4538            (a_i, b_i)
4539        };
4540
4541        // Sweep both labels (s = ±1), both tails of the predictor, and a
4542        // non-unit weight so every sign/scale path of the Mills algebra is
4543        // exercised. Points stay clear of the deep-tail asymptote where a
4544        // central-difference reference loses its own accuracy.
4545        let cases: [(f64, f64, f64); 12] = [
4546            (-1.6, 1.0, 1.0),
4547            (-0.7, 1.0, 1.0),
4548            (0.0, 1.0, 1.0),
4549            (0.9, 1.0, 1.0),
4550            (1.8, 1.0, 1.0),
4551            (-1.4, 0.0, 1.0),
4552            (-0.3, 0.0, 1.0),
4553            (0.0, 0.0, 1.0),
4554            (0.6, 0.0, 1.0),
4555            (1.5, 0.0, 1.0),
4556            (0.4, 1.0, 0.75),
4557            (-0.8, 0.0, 1.3),
4558        ];
4559        // Fifth-order central stencils; `h` chosen near the f64 sweet spot for
4560        // first/second derivatives of a smooth O(1) function.
4561        let h = 1e-3_f64;
4562        for (e, y, w) in cases {
4563            let (a_ana, b_ana) = ab_of(e, y, w);
4564
4565            let fp2 = neglog_of(e + 2.0 * h, y, w);
4566            let fp1 = neglog_of(e + h, y, w);
4567            let f0 = neglog_of(e, y, w);
4568            let fm1 = neglog_of(e - h, y, w);
4569            let fm2 = neglog_of(e - 2.0 * h, y, w);
4570
4571            // 5-point central first derivative: O(h⁴).
4572            let d1_fd = (-fp2 + 8.0 * fp1 - 8.0 * fm1 + fm2) / (12.0 * h);
4573            // 5-point central second derivative: O(h⁴).
4574            let d2_fd = (-fp2 + 16.0 * fp1 - 30.0 * f0 + 16.0 * fm1 - fm2) / (12.0 * h * h);
4575
4576            let a_abs = (a_ana - d1_fd).abs();
4577            let a_rel = a_abs / a_ana.abs().max(1.0);
4578            assert!(
4579                a_abs <= 5e-8 || a_rel <= 5e-8,
4580                "Mills A (∂neglog/∂e) drift at e={e} y={y} w={w}: \
4581                 analytic={a_ana:.17e} fd={d1_fd:.17e} abs={a_abs:.3e} rel={a_rel:.3e}"
4582            );
4583
4584            let b_abs = (b_ana - d2_fd).abs();
4585            let b_rel = b_abs / b_ana.abs().max(1.0);
4586            assert!(
4587                b_abs <= 5e-6 || b_rel <= 5e-6,
4588                "Mills B (∂²neglog/∂e²) drift at e={e} y={y} w={w}: \
4589                 analytic={b_ana:.17e} fd={d2_fd:.17e} abs={b_abs:.3e} rel={b_rel:.3e}"
4590            );
4591        }
4592    }
4593
4594    #[test]
4595    pub(crate) fn generated_source_interprets_compact_canonical_phase_streams() {
4596        let source = generated_row_kernel_source();
4597        assert!(!source.contains("__BMS_FLEX_CALIBRATION_ORDER2__"));
4598        assert!(!source.contains("__BMS_FLEX_ORDER2_FINALIZER__"));
4599        assert!(!source.contains("__BMS_FLEX_ROW_THREADS__"));
4600        assert!(source.contains("for (int u = 1; u < r; ++u)"));
4601        assert!(source.contains("for (int v = u; v < r; ++v)"));
4602        assert!(source.contains("Canonical implicit-first stage complete"));
4603        assert!(source.contains("double *F_u = out_grad + row_r_base"));
4604        assert!(source.contains("double *F_au = row_f_au + row_r_base"));
4605        assert!(source.contains("double *F_uv = out_hess + row_rr_base"));
4606        for forbidden in [
4607            "MAX_R",
4608            "double F_u[",
4609            "double F_au[",
4610            "double F_uv[",
4611            "double a_u[",
4612            "double a_uv[",
4613            "double bar_e_u[",
4614        ] {
4615            assert!(
4616                !source.contains(forbidden),
4617                "generated row source restored width-bound scratch: {forbidden}"
4618            );
4619        }
4620        for forbidden in [
4621            "MAX_R",
4622            "double row_dir[",
4623            "double action[",
4624            "bms_flex_row_hvp_partial_packed",
4625            "bms_flex_row_diag_partial_packed",
4626            "bms_flex_row_pack_upper",
4627        ] {
4628            assert!(
4629                !HVP_KERNEL_SOURCE.contains(forbidden),
4630                "HVP source restored a dead or width-bound path: {forbidden}"
4631            );
4632        }
4633        assert!(HVP_KERNEL_SOURCE.contains("bms_flex_primary_direction"));
4634        assert!(HVP_KERNEL_SOURCE.contains("direction_q[MAX_MULTI_RHS]"));
4635        assert!(HVP_KERNEL_SOURCE.contains("action_g[MAX_MULTI_RHS]"));
4636        let mut cursor = 0usize;
4637        for marker in [
4638            "canonical calibration phase: InterceptFirst",
4639            "canonical calibration phase: InterceptSecond",
4640            "canonical calibration phase: PrimaryFirstAndInterceptSecond",
4641            "canonical calibration phase: PrimaryPairSecond",
4642            "canonical finalizer phase: ImplicitFirst",
4643            "canonical finalizer phase: ImplicitFirstComplete",
4644            "canonical finalizer phase: ImplicitSecond",
4645            "canonical finalizer phase: ObservedFirst",
4646            "canonical finalizer phase: ObservedScoreSensitivity",
4647            "canonical finalizer phase: ObservedSecond",
4648            "canonical finalizer phase: NegLogFirst",
4649        ] {
4650            let relative = source[cursor..]
4651                .find(marker)
4652                .unwrap_or_else(|| panic!("generated CUDA source omitted phase {marker}"));
4653            cursor += relative + marker.len();
4654        }
4655        assert!(
4656            source.len() < 40_000,
4657            "generated CUDA source unexpectedly bloated"
4658        );
4659    }
4660
4661    // ── Phase-3 HVP / diagonal CPU oracles + GPU parity tests ────────────────
4662
4663    /// CPU oracle for [`launch_bms_flex_row_hvp`]. Mirrors the device kernel
4664    /// element-for-element so the GPU parity test runs against the same algebra.
4665    pub(crate) fn cpu_oracle_bms_flex_row_hvp(
4666        row_hessians: &[f64],
4667        marginal_design: &[f64],
4668        logslope_design: &[f64],
4669        block: &BmsFlexBlockLayout,
4670        primary: &BmsFlexPrimaryLayout,
4671        n: usize,
4672        v: &[f64],
4673    ) -> Vec<f64> {
4674        let r = primary.r;
4675        let p_m = block.p_m;
4676        let p_g = block.p_g;
4677        assert_eq!(v.len(), block.p_total);
4678        assert_eq!(row_hessians.len(), n * r * r);
4679        assert_eq!(marginal_design.len(), n * p_m);
4680        assert_eq!(logslope_design.len(), n * p_g);
4681        let mut out = vec![0.0_f64; block.p_total];
4682        let mut row_dir = vec![0.0_f64; r];
4683        let mut action = vec![0.0_f64; r];
4684        for row in 0..n {
4685            let mrow = &marginal_design[row * p_m..(row + 1) * p_m];
4686            let grow = &logslope_design[row * p_g..(row + 1) * p_g];
4687            let mut acc_q = 0.0_f64;
4688            for j in 0..p_m {
4689                acc_q += mrow[j] * v[j];
4690            }
4691            let mut acc_g = 0.0_f64;
4692            for j in 0..p_g {
4693                acc_g += grow[j] * v[p_m + j];
4694            }
4695            row_dir[0] = acc_q;
4696            row_dir[1] = acc_g;
4697            if let (Some(prange), Some(brange)) = (primary.h.as_ref(), block.h.as_ref()) {
4698                for (k, ii) in prange.clone().enumerate() {
4699                    row_dir[ii] = v[brange.start + k];
4700                }
4701            }
4702            if let (Some(prange), Some(brange)) = (primary.w.as_ref(), block.w.as_ref()) {
4703                for (k, ii) in prange.clone().enumerate() {
4704                    row_dir[ii] = v[brange.start + k];
4705                }
4706            }
4707            let h_slice = &row_hessians[row * r * r..(row + 1) * r * r];
4708            for u in 0..r {
4709                let mut acc = 0.0_f64;
4710                for v_idx in 0..r {
4711                    acc += h_slice[u * r + v_idx] * row_dir[v_idx];
4712                }
4713                action[u] = acc;
4714            }
4715            let a0 = action[0];
4716            for j in 0..p_m {
4717                out[j] += a0 * mrow[j];
4718            }
4719            let a1 = action[1];
4720            for j in 0..p_g {
4721                out[p_m + j] += a1 * grow[j];
4722            }
4723            if let (Some(prange), Some(brange)) = (primary.h.as_ref(), block.h.as_ref()) {
4724                for (k, ii) in prange.clone().enumerate() {
4725                    out[brange.start + k] += action[ii];
4726                }
4727            }
4728            if let (Some(prange), Some(brange)) = (primary.w.as_ref(), block.w.as_ref()) {
4729                for (k, ii) in prange.clone().enumerate() {
4730                    out[brange.start + k] += action[ii];
4731                }
4732            }
4733        }
4734        out
4735    }
4736
4737    pub(crate) fn cpu_oracle_bms_flex_row_diagonal(
4738        row_hessians: &[f64],
4739        marginal_design: &[f64],
4740        logslope_design: &[f64],
4741        block: &BmsFlexBlockLayout,
4742        primary: &BmsFlexPrimaryLayout,
4743        n: usize,
4744    ) -> Vec<f64> {
4745        let r = primary.r;
4746        let p_m = block.p_m;
4747        let p_g = block.p_g;
4748        let mut out = vec![0.0_f64; block.p_total];
4749        for row in 0..n {
4750            let h_slice = &row_hessians[row * r * r..(row + 1) * r * r];
4751            let h00 = h_slice[0];
4752            let h11 = h_slice[r + 1];
4753            let mrow = &marginal_design[row * p_m..(row + 1) * p_m];
4754            let grow = &logslope_design[row * p_g..(row + 1) * p_g];
4755            for j in 0..p_m {
4756                out[j] += h00 * mrow[j] * mrow[j];
4757            }
4758            for j in 0..p_g {
4759                out[p_m + j] += h11 * grow[j] * grow[j];
4760            }
4761            if let (Some(prange), Some(brange)) = (primary.h.as_ref(), block.h.as_ref()) {
4762                for (k, ii) in prange.clone().enumerate() {
4763                    out[brange.start + k] += h_slice[ii * r + ii];
4764                }
4765            }
4766            if let (Some(prange), Some(brange)) = (primary.w.as_ref(), block.w.as_ref()) {
4767                for (k, ii) in prange.clone().enumerate() {
4768                    out[brange.start + k] += h_slice[ii * r + ii];
4769                }
4770            }
4771        }
4772        out
4773    }
4774
4775    pub(crate) fn cpu_oracle_bms_flex_row_joint_gradient(
4776        row_neglog: &[f64],
4777        row_grad: &[f64],
4778        marginal_design: &[f64],
4779        logslope_design: &[f64],
4780        block: &BmsFlexBlockLayout,
4781        primary: &BmsFlexPrimaryLayout,
4782        n: usize,
4783    ) -> (f64, Vec<f64>) {
4784        let r = primary.r;
4785        assert_eq!(row_neglog.len(), n);
4786        assert_eq!(row_grad.len(), n * r);
4787        assert_eq!(marginal_design.len(), n * block.p_m);
4788        assert_eq!(logslope_design.len(), n * block.p_g);
4789        let mut log_likelihood = 0.0_f64;
4790        let mut gradient = vec![0.0_f64; block.p_total];
4791        for row in 0..n {
4792            log_likelihood -= row_neglog[row];
4793            let grow = &row_grad[row * r..(row + 1) * r];
4794            for j in 0..block.p_m {
4795                gradient[j] -= grow[0] * marginal_design[row * block.p_m + j];
4796            }
4797            for j in 0..block.p_g {
4798                gradient[block.p_m + j] -= grow[1] * logslope_design[row * block.p_g + j];
4799            }
4800            if let (Some(primary_h), Some(block_h)) = (primary.h.as_ref(), block.h.as_ref()) {
4801                for (offset, primary_idx) in primary_h.clone().enumerate() {
4802                    gradient[block_h.start + offset] -= grow[primary_idx];
4803                }
4804            }
4805            if let (Some(primary_w), Some(block_w)) = (primary.w.as_ref(), block.w.as_ref()) {
4806                for (offset, primary_idx) in primary_w.clone().enumerate() {
4807                    gradient[block_w.start + offset] -= grow[primary_idx];
4808                }
4809            }
4810        }
4811        (log_likelihood, gradient)
4812    }
4813
4814    #[test]
4815    fn cpu_joint_gradient_oracle_pins_score_sign_and_active_hw_pullback() {
4816        let n = 2_usize;
4817        let r = 5_usize;
4818        let block = BmsFlexBlockLayout {
4819            p_m: 2,
4820            p_g: 1,
4821            h: Some(3..5),
4822            w: Some(5..6),
4823            p_total: 6,
4824        };
4825        let primary = BmsFlexPrimaryLayout {
4826            h: Some(2..4),
4827            w: Some(4..5),
4828            r,
4829        };
4830        let row_neglog = [1.25, 0.75];
4831        let row_grad = [
4832            2.0, -3.0, 5.0, -7.0, 11.0, // row 0
4833            -13.0, 17.0, -19.0, 23.0, -29.0, // row 1
4834        ];
4835        let marginal = [1.0, 2.0, -0.5, 3.0];
4836        let logslope = [4.0, -2.0];
4837        let (log_likelihood, gradient) = cpu_oracle_bms_flex_row_joint_gradient(
4838            &row_neglog,
4839            &row_grad,
4840            &marginal,
4841            &logslope,
4842            &block,
4843            &primary,
4844            n,
4845        );
4846        assert_eq!(log_likelihood, -2.0);
4847        assert_eq!(
4848            gradient,
4849            vec![-8.5, 35.0, 46.0, 14.0, -16.0, 18.0],
4850            "joint output must be the score/log-likelihood sign, with h/w direct slots"
4851        );
4852    }
4853
4854    /// Hand-construct a small symmetric per-row Hessian + small designs and
4855    /// verify the CPU oracle satisfies the expected algebra. Platform-
4856    /// independent (runs on macOS / Linux without CUDA).
4857    #[test]
4858    pub(crate) fn cpu_oracle_hvp_matches_hand_computation_no_hw() {
4859        let n = 4_usize;
4860        let r = 4_usize; // q, logslope, h(1), w(1)
4861        let p_m = 2_usize;
4862        let p_g = 2_usize;
4863        let p_h_dim = 1_usize;
4864        let p_w_dim = 1_usize;
4865        let p_total = p_m + p_g + p_h_dim + p_w_dim;
4866        let block = BmsFlexBlockLayout {
4867            p_m,
4868            p_g,
4869            h: Some(p_m + p_g..p_m + p_g + p_h_dim),
4870            w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
4871            p_total,
4872        };
4873        let primary = BmsFlexPrimaryLayout {
4874            h: Some(2..3),
4875            w: Some(3..4),
4876            r,
4877        };
4878        // Symmetric per-row Hessian: H_row[u,v] = (row + 1) * (1 + u + 2v) symmetrised.
4879        let mut row_hessians = vec![0.0_f64; n * r * r];
4880        for row in 0..n {
4881            for u in 0..r {
4882                for v in u..r {
4883                    let val = ((row + 1) as f64) * (1.0 + (u as f64) + 2.0 * (v as f64));
4884                    row_hessians[row * r * r + u * r + v] = val;
4885                    row_hessians[row * r * r + v * r + u] = val;
4886                }
4887            }
4888        }
4889        let mut marginal = vec![0.0_f64; n * p_m];
4890        for row in 0..n {
4891            for j in 0..p_m {
4892                marginal[row * p_m + j] = 0.5 + (row as f64) * 0.1 - (j as f64) * 0.2;
4893            }
4894        }
4895        let mut logslope = vec![0.0_f64; n * p_g];
4896        for row in 0..n {
4897            for j in 0..p_g {
4898                logslope[row * p_g + j] = -0.3 + (row as f64) * 0.05 + (j as f64) * 0.15;
4899            }
4900        }
4901        let v: Vec<f64> = (0..p_total).map(|i| 0.1 + (i as f64) * 0.25).collect();
4902        let out = cpu_oracle_bms_flex_row_hvp(
4903            &row_hessians,
4904            &marginal,
4905            &logslope,
4906            &block,
4907            &primary,
4908            n,
4909            &v,
4910        );
4911        // Hand check the first marginal slot: out[0] = Σ_row action[0]·mrow[0].
4912        let mut expect_out_0 = 0.0_f64;
4913        for row in 0..n {
4914            let mrow = &marginal[row * p_m..(row + 1) * p_m];
4915            let grow = &logslope[row * p_g..(row + 1) * p_g];
4916            let mut row_dir = vec![0.0_f64; r];
4917            row_dir[0] = mrow[0] * v[0] + mrow[1] * v[1];
4918            row_dir[1] = grow[0] * v[p_m] + grow[1] * v[p_m + 1];
4919            row_dir[2] = v[p_m + p_g];
4920            row_dir[3] = v[p_m + p_g + p_h_dim];
4921            let h_slice = &row_hessians[row * r * r..(row + 1) * r * r];
4922            let mut action0 = 0.0_f64;
4923            // h_slice is the row-major r×r Hessian for this row; we want
4924            // row 0, i.e. entries (0, vv) for vv in 0..r, which lives at
4925            // `vv` in the flat layout.
4926            for vv in 0..r {
4927                action0 += h_slice[vv] * row_dir[vv];
4928            }
4929            expect_out_0 += action0 * mrow[0];
4930        }
4931        assert!(
4932            (out[0] - expect_out_0).abs() < 1e-12,
4933            "cpu oracle HVP out[0] mismatch: {} vs hand-check {}",
4934            out[0],
4935            expect_out_0
4936        );
4937        assert!(out.iter().all(|x| x.is_finite()));
4938        assert_eq!(out.len(), p_total);
4939    }
4940
4941    /// Diagonal oracle equals the explicit per-row design² accumulator.
4942    #[test]
4943    pub(crate) fn cpu_oracle_diagonal_matches_hand_computation() {
4944        let n = 3_usize;
4945        let r = 4_usize;
4946        let p_m = 2_usize;
4947        let p_g = 2_usize;
4948        let p_h_dim = 1_usize;
4949        let p_w_dim = 1_usize;
4950        let p_total = p_m + p_g + p_h_dim + p_w_dim;
4951        let block = BmsFlexBlockLayout {
4952            p_m,
4953            p_g,
4954            h: Some(p_m + p_g..p_m + p_g + p_h_dim),
4955            w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
4956            p_total,
4957        };
4958        let primary = BmsFlexPrimaryLayout {
4959            h: Some(2..3),
4960            w: Some(3..4),
4961            r,
4962        };
4963        let mut row_hessians = vec![0.0_f64; n * r * r];
4964        for row in 0..n {
4965            for u in 0..r {
4966                row_hessians[row * r * r + u * r + u] = 1.0 + (row as f64) + (u as f64) * 0.5;
4967            }
4968        }
4969        let mut marginal = vec![0.0_f64; n * p_m];
4970        let mut logslope = vec![0.0_f64; n * p_g];
4971        for row in 0..n {
4972            for j in 0..p_m {
4973                marginal[row * p_m + j] = 0.2 + (row as f64) * 0.3 + (j as f64) * 0.1;
4974            }
4975            for j in 0..p_g {
4976                logslope[row * p_g + j] = -0.4 + (row as f64) * 0.1 + (j as f64) * 0.2;
4977            }
4978        }
4979        let out = cpu_oracle_bms_flex_row_diagonal(
4980            &row_hessians,
4981            &marginal,
4982            &logslope,
4983            &block,
4984            &primary,
4985            n,
4986        );
4987        // Hand check: out[0] = Σ_row H[row,0,0] · marginal[row,0]^2.
4988        let mut expect = 0.0_f64;
4989        for row in 0..n {
4990            let h00 = row_hessians[row * r * r];
4991            expect += h00 * marginal[row * p_m].powi(2);
4992        }
4993        assert!(
4994            (out[0] - expect).abs() < 1e-12,
4995            "out[0] {} vs {}",
4996            out[0],
4997            expect
4998        );
4999        // h slot = sum of H[row, 2, 2] across rows.
5000        let mut expect_h = 0.0_f64;
5001        for row in 0..n {
5002            expect_h += row_hessians[row * r * r + 2 * r + 2];
5003        }
5004        let h_slot = p_m + p_g;
5005        assert!(
5006            (out[h_slot] - expect_h).abs() < 1e-12,
5007            "h slot {} vs {}",
5008            out[h_slot],
5009            expect_h
5010        );
5011    }
5012
5013    /// Mandatory GPU↔CPU parity for every device-resident row consumer at
5014    /// `r=33`, with both direct h/w blocks active.
5015    /// Hand-constructs a small `DeviceResidentRowHess` by
5016    /// allocating the device slices directly, uploading the same arrays the
5017    /// CPU oracle consumes, then dispatching the device kernels.
5018    #[test]
5019    pub(crate) fn bms_flex_row_r33_consumers_match_cpu_oracles_when_cuda_available() {
5020        if cuda_runtime_for_test("bms_flex_row_r33_consumers_match_cpu_oracles_when_cuda_available")
5021            .is_none()
5022        {
5023            return;
5024        }
5025        let n = 3_usize;
5026        let p_h_dim = 16_usize;
5027        let p_w_dim = 15_usize;
5028        let r = 2 + p_h_dim + p_w_dim;
5029        let p_m = 2_usize;
5030        let p_g = 2_usize;
5031        let p_total = p_m + p_g + p_h_dim + p_w_dim;
5032        let block = BmsFlexBlockLayout {
5033            p_m,
5034            p_g,
5035            h: Some(p_m + p_g..p_m + p_g + p_h_dim),
5036            w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
5037            p_total,
5038        };
5039        let primary = BmsFlexPrimaryLayout {
5040            h: Some(2..2 + p_h_dim),
5041            w: Some(2 + p_h_dim..2 + p_h_dim + p_w_dim),
5042            r,
5043        };
5044        let mut row_hessians = vec![0.0_f64; n * r * r];
5045        for row in 0..n {
5046            for u in 0..r {
5047                for v in u..r {
5048                    let val = 0.001 * ((row + 1) as f64) * (1.0 + (u as f64) + 2.0 * (v as f64));
5049                    row_hessians[row * r * r + u * r + v] = val;
5050                    row_hessians[row * r * r + v * r + u] = val;
5051                }
5052            }
5053        }
5054        let mut marginal = vec![0.0_f64; n * p_m];
5055        for row in 0..n {
5056            for j in 0..p_m {
5057                marginal[row * p_m + j] = 0.5 + (row as f64) * 0.1 - (j as f64) * 0.2;
5058            }
5059        }
5060        let mut logslope = vec![0.0_f64; n * p_g];
5061        for row in 0..n {
5062            for j in 0..p_g {
5063                logslope[row * p_g + j] = -0.3 + (row as f64) * 0.05 + (j as f64) * 0.15;
5064            }
5065        }
5066        let v: Vec<f64> = (0..p_total).map(|i| 0.1 + (i as f64) * 0.25).collect();
5067        let cpu_hvp = cpu_oracle_bms_flex_row_hvp(
5068            &row_hessians,
5069            &marginal,
5070            &logslope,
5071            &block,
5072            &primary,
5073            n,
5074            &v,
5075        );
5076        let cpu_diag = cpu_oracle_bms_flex_row_diagonal(
5077            &row_hessians,
5078            &marginal,
5079            &logslope,
5080            &block,
5081            &primary,
5082            n,
5083        );
5084        let row_neglog = (0..n)
5085            .map(|row| 0.25 + 0.125 * row as f64)
5086            .collect::<Vec<_>>();
5087        let row_grad = (0..n * r)
5088            .map(|index| {
5089                let row = index / r;
5090                let primary_idx = index % r;
5091                (row as f64 + 0.75) * (primary_idx as f64 - 1.25)
5092            })
5093            .collect::<Vec<_>>();
5094        let (cpu_log_likelihood, cpu_gradient) = cpu_oracle_bms_flex_row_joint_gradient(
5095            &row_neglog,
5096            &row_grad,
5097            &marginal,
5098            &logslope,
5099            &block,
5100            &primary,
5101            n,
5102        );
5103        let mut cpu_dense = vec![0.0_f64; p_total * p_total];
5104        for column in 0..p_total {
5105            let mut basis = vec![0.0_f64; p_total];
5106            basis[column] = 1.0;
5107            let image = cpu_oracle_bms_flex_row_hvp(
5108                &row_hessians,
5109                &marginal,
5110                &logslope,
5111                &block,
5112                &primary,
5113                n,
5114                &basis,
5115            );
5116            for (row, value) in image.into_iter().enumerate() {
5117                cpu_dense[row * p_total + column] = value;
5118            }
5119        }
5120
5121        // Allocate a DeviceResidentRowHess by hand using the HVP backend's
5122        // stream + module so we don't need to drive the full BMS row kernel.
5123        // Past the lossless Auto-resolution gate above: a probe/upload failure
5124        // here is a real device fault on a CUDA host, not a no-CUDA skip. Fail
5125        // loud (the device-PCG skip-pass class, eee12f6b2) — the old arms
5126        // returned and the test passed while exercising nothing.
5127        let backend = HvpKernelBackend::probe()
5128            .expect("[bms_flex_row hvp parity] backend probe must succeed on CUDA host");
5129        let stream = backend.stream.clone();
5130        let d_h = stream
5131            .clone_htod(&row_hessians)
5132            .expect("[bms_flex_row hvp parity] upload h must succeed on CUDA host");
5133        let d_m = stream
5134            .clone_htod(&marginal)
5135            .expect("[bms_flex_row hvp parity] upload marg must succeed on CUDA host");
5136        let d_g = stream
5137            .clone_htod(&logslope)
5138            .expect("[bms_flex_row hvp parity] upload logslope must succeed on CUDA host");
5139        let storage = DeviceResidentRowHess {
5140            neglog: stream
5141                .clone_htod(&row_neglog)
5142                .expect("[bms_flex_row hvp parity] upload neglog"),
5143            grad: stream
5144                .clone_htod(&row_grad)
5145                .expect("[bms_flex_row hvp parity] upload grad"),
5146            hess: d_h,
5147            marginal_design: d_m,
5148            logslope_design: d_g,
5149            n,
5150            r,
5151            block: block.clone(),
5152            primary: primary.clone(),
5153
5154            bytes: ((n + n * r + n * r * r + n * p_m + n * p_g) * std::mem::size_of::<f64>())
5155                as u64,
5156        };
5157        let gpu_hvp =
5158            launch_bms_flex_row_hvp(&storage, &v).expect("HVP kernel must launch on CUDA host");
5159        let gpu_diag = launch_bms_flex_row_diagonal(&storage)
5160            .expect("diagonal kernel must launch on CUDA host");
5161        let gpu_joint = launch_bms_flex_row_joint_gradient(&storage)
5162            .expect("joint-gradient kernel must launch on CUDA host");
5163        let gpu_dense = launch_bms_flex_row_dense(&storage)
5164            .expect("dense kernel must launch at r=33 on CUDA host");
5165        assert_eq!(gpu_hvp.len(), cpu_hvp.len());
5166        assert_eq!(gpu_diag.len(), cpu_diag.len());
5167        assert_eq!(gpu_joint.gradient.len(), cpu_gradient.len());
5168        assert!(
5169            (gpu_joint.log_likelihood - cpu_log_likelihood).abs() <= 1e-12,
5170            "loglik: cpu={} gpu={}",
5171            cpu_log_likelihood,
5172            gpu_joint.log_likelihood
5173        );
5174        for i in 0..p_total {
5175            let diff = (cpu_hvp[i] - gpu_hvp[i]).abs();
5176            assert!(
5177                diff <= 1e-10,
5178                "HVP[{i}]: cpu={} gpu={} |Δ|={diff:.3e}",
5179                cpu_hvp[i],
5180                gpu_hvp[i]
5181            );
5182            let ddiff = (cpu_diag[i] - gpu_diag[i]).abs();
5183            assert!(
5184                ddiff <= 1e-10,
5185                "diag[{i}]: cpu={} gpu={} |Δ|={ddiff:.3e}",
5186                cpu_diag[i],
5187                gpu_diag[i]
5188            );
5189            let gdiff = (cpu_gradient[i] - gpu_joint.gradient[i]).abs();
5190            assert!(
5191                gdiff <= 1e-10,
5192                "joint gradient[{i}]: cpu={} gpu={} |Δ|={gdiff:.3e}",
5193                cpu_gradient[i],
5194                gpu_joint.gradient[i]
5195            );
5196        }
5197        assert_eq!(gpu_dense.len(), cpu_dense.len());
5198        for (index, (&cpu, &gpu)) in cpu_dense.iter().zip(&gpu_dense).enumerate() {
5199            let tolerance = 1e-10 * (1.0 + cpu.abs());
5200            assert!(
5201                (cpu - gpu).abs() <= tolerance,
5202                "dense[{index}] at r=33: cpu={cpu} gpu={gpu} tolerance={tolerance}"
5203            );
5204        }
5205    }
5206
5207    #[test]
5208    pub(crate) fn bms_flex_row_hvp_multi_scratch_is_bounded_at_large_scale_shape() {
5209        let n = 195_000_usize;
5210        let r = 20_usize;
5211        let p_total = 44_usize;
5212        let rhs_count = 4_usize;
5213        let scratch = bms_flex_row_hvp_multi_scratch_bytes_for_shape(n, p_total, rhs_count)
5214            .expect("large-scale multi-RHS scratch budget");
5215        let per_rhs_full_row_cache =
5216            (n * r * r * std::mem::size_of::<f64>()) as u64 * rhs_count as u64;
5217        assert!(
5218            scratch < per_rhs_full_row_cache / 100,
5219            "multi-RHS scratch must tile by row chunks instead of materializing \
5220             a row-Hessian copy per RHS: scratch={scratch} full_per_rhs={per_rhs_full_row_cache}"
5221        );
5222        assert!(
5223            bms_flex_row_hvp_multi_scratch_bytes_for_shape(
5224                n,
5225                p_total,
5226                BMS_FLEX_ROW_HVP_MAX_RHS + 1
5227            )
5228            .is_err(),
5229            "multi-RHS launch must reject unbounded RHS counts"
5230        );
5231    }
5232
5233    #[test]
5234    pub(crate) fn bms_flex_row_hvp_multi_kernel_matches_cpu_oracle_when_cuda_available() {
5235        if cuda_runtime_for_test("bms_flex_row hvp_multi parity").is_none() {
5236            return;
5237        }
5238        let n = 5_usize;
5239        let r = 4_usize;
5240        let p_m = 2_usize;
5241        let p_g = 2_usize;
5242        let p_h_dim = 1_usize;
5243        let p_w_dim = 1_usize;
5244        let p_total = p_m + p_g + p_h_dim + p_w_dim;
5245        let rhs_count = 3_usize;
5246        let block = BmsFlexBlockLayout {
5247            p_m,
5248            p_g,
5249            h: Some(p_m + p_g..p_m + p_g + p_h_dim),
5250            w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
5251            p_total,
5252        };
5253        let primary = BmsFlexPrimaryLayout {
5254            h: Some(2..3),
5255            w: Some(3..4),
5256            r,
5257        };
5258        let mut row_hessians = vec![0.0_f64; n * r * r];
5259        for row in 0..n {
5260            for u in 0..r {
5261                for v in u..r {
5262                    let val = ((row + 1) as f64) * (1.0 + (u as f64) + 2.0 * (v as f64));
5263                    row_hessians[row * r * r + u * r + v] = val;
5264                    row_hessians[row * r * r + v * r + u] = val;
5265                }
5266            }
5267        }
5268        let mut marginal = vec![0.0_f64; n * p_m];
5269        let mut logslope = vec![0.0_f64; n * p_g];
5270        for row in 0..n {
5271            for j in 0..p_m {
5272                marginal[row * p_m + j] = 0.5 + (row as f64) * 0.1 - (j as f64) * 0.2;
5273            }
5274            for j in 0..p_g {
5275                logslope[row * p_g + j] = -0.3 + (row as f64) * 0.05 + (j as f64) * 0.15;
5276            }
5277        }
5278        let mut v_rhs = vec![0.0_f64; rhs_count * p_total];
5279        for rhs in 0..rhs_count {
5280            for j in 0..p_total {
5281                let seed = (rhs as f64) * 0.37 + (j as f64) * 0.19 + 0.4;
5282                v_rhs[rhs * p_total + j] = seed.sin() * 0.4 + seed.cos() * 0.2;
5283            }
5284        }
5285
5286        // Past the lossless Auto-resolution gate: a probe/upload failure here is a
5287        // real device fault on a CUDA host, not a no-CUDA skip — fail loud
5288        // (device-PCG skip-pass class, eee12f6b2).
5289        let backend = HvpKernelBackend::probe()
5290            .expect("[bms_flex_row hvp_multi parity] backend probe must succeed on CUDA host");
5291        let stream = backend.stream.clone();
5292        let d_h = stream
5293            .clone_htod(&row_hessians)
5294            .expect("[bms_flex_row hvp_multi parity] upload h must succeed on CUDA host");
5295        let d_m = stream
5296            .clone_htod(&marginal)
5297            .expect("[bms_flex_row hvp_multi parity] upload marg must succeed on CUDA host");
5298        let d_g = stream
5299            .clone_htod(&logslope)
5300            .expect("[bms_flex_row hvp_multi parity] upload logslope must succeed on CUDA host");
5301        let storage = DeviceResidentRowHess {
5302            neglog: stream
5303                .alloc_zeros::<f64>(n)
5304                .expect("[bms_flex_row hvp_multi parity] alloc neglog"),
5305            grad: stream
5306                .alloc_zeros::<f64>(n * r)
5307                .expect("[bms_flex_row hvp_multi parity] alloc grad"),
5308            hess: d_h,
5309            marginal_design: d_m,
5310            logslope_design: d_g,
5311            n,
5312            r,
5313            block: block.clone(),
5314            primary: primary.clone(),
5315
5316            bytes: ((n + n * r + n * r * r + n * p_m + n * p_g) * std::mem::size_of::<f64>())
5317                as u64,
5318        };
5319        let scratch = bms_flex_row_hvp_multi_scratch_bytes_for_shape(n, p_total, rhs_count)
5320            .expect("storage scratch budget");
5321        assert!(
5322            scratch < storage.bytes,
5323            "multi-RHS scratch should stay below resident cache bytes"
5324        );
5325        let gpu = launch_bms_flex_row_hvp_multi(&storage, &v_rhs, rhs_count)
5326            .expect("multi-RHS HVP kernel must launch on CUDA host");
5327        assert_eq!(gpu.len(), rhs_count * p_total);
5328        for rhs in 0..rhs_count {
5329            let v = &v_rhs[rhs * p_total..(rhs + 1) * p_total];
5330            let cpu = cpu_oracle_bms_flex_row_hvp(
5331                &row_hessians,
5332                &marginal,
5333                &logslope,
5334                &block,
5335                &primary,
5336                n,
5337                v,
5338            );
5339            let single = launch_bms_flex_row_hvp(&storage, v)
5340                .expect("single-RHS HVP kernel must launch on CUDA host");
5341            for j in 0..p_total {
5342                let got = gpu[rhs * p_total + j];
5343                let diff = (cpu[j] - got).abs();
5344                assert!(
5345                    diff <= 1e-10,
5346                    "multi-RHS HVP rhs={rhs} j={j}: cpu={} gpu={} |diff|={diff:.3e}",
5347                    cpu[j],
5348                    got
5349                );
5350                assert_eq!(
5351                    got, single[j],
5352                    "multi-RHS and single-RHS host launch diverged at rhs={rhs} j={j}"
5353                );
5354            }
5355        }
5356    }
5357
5358    /// Parity for the third launch mode — device-output HVP
5359    /// ([`launch_bms_flex_row_hvp_into_device`]) — which the
5360    /// `run_bms_flex_row_partial_reduce` unification routes through the same
5361    /// partial+reduce engine as the host-returning `_hvp` / `_diagonal`
5362    /// adapters. Confirms that keeping the result on-stream (no internal sync /
5363    /// DtoH) reaches bit-identical output to both the CPU oracle and the
5364    /// host-out adapter, so the engine's mode/output split is faithful.
5365    ///
5366    /// Skips cleanly on non-Linux / no-CUDA hosts using the convention shared
5367    /// with the sibling parity tests.
5368    #[test]
5369    pub(crate) fn bms_flex_row_hvp_into_device_matches_cpu_oracle_and_host_out() {
5370        #[cfg(not(target_os = "linux"))]
5371        {
5372            eprintln!(
5373                "[bms_flex_row hvp_into_device parity] non-Linux host — skipping \
5374                 CUDA parity (CPU oracle exercised by sibling tests)"
5375            );
5376        }
5377        #[cfg(target_os = "linux")]
5378        {
5379            if cuda_runtime_for_test("bms_flex_row hvp_into_device parity").is_none() {
5380                return;
5381            }
5382            let n = 4_usize;
5383            let r = 4_usize;
5384            let p_m = 2_usize;
5385            let p_g = 2_usize;
5386            let p_h_dim = 1_usize;
5387            let p_w_dim = 1_usize;
5388            let p_total = p_m + p_g + p_h_dim + p_w_dim;
5389            let block = BmsFlexBlockLayout {
5390                p_m,
5391                p_g,
5392                h: Some(p_m + p_g..p_m + p_g + p_h_dim),
5393                w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
5394                p_total,
5395            };
5396            let primary = BmsFlexPrimaryLayout {
5397                h: Some(2..3),
5398                w: Some(3..4),
5399                r,
5400            };
5401            let mut row_hessians = vec![0.0_f64; n * r * r];
5402            for row in 0..n {
5403                for u in 0..r {
5404                    for v in u..r {
5405                        let val = ((row + 1) as f64) * (1.0 + (u as f64) + 2.0 * (v as f64));
5406                        row_hessians[row * r * r + u * r + v] = val;
5407                        row_hessians[row * r * r + v * r + u] = val;
5408                    }
5409                }
5410            }
5411            let mut marginal = vec![0.0_f64; n * p_m];
5412            for row in 0..n {
5413                for j in 0..p_m {
5414                    marginal[row * p_m + j] = 0.5 + (row as f64) * 0.1 - (j as f64) * 0.2;
5415                }
5416            }
5417            let mut logslope = vec![0.0_f64; n * p_g];
5418            for row in 0..n {
5419                for j in 0..p_g {
5420                    logslope[row * p_g + j] = -0.3 + (row as f64) * 0.05 + (j as f64) * 0.15;
5421                }
5422            }
5423            let v: Vec<f64> = (0..p_total).map(|i| 0.1 + (i as f64) * 0.25).collect();
5424            let cpu_hvp = cpu_oracle_bms_flex_row_hvp(
5425                &row_hessians,
5426                &marginal,
5427                &logslope,
5428                &block,
5429                &primary,
5430                n,
5431                &v,
5432            );
5433
5434            // Past the lossless Auto-resolution gate: probe/upload failures are
5435            // real device faults on a CUDA host — fail loud (device-PCG class).
5436            let backend = HvpKernelBackend::probe().expect(
5437                "[bms_flex_row hvp_into_device parity] backend probe must succeed on CUDA host",
5438            );
5439            let stream = backend.stream.clone();
5440            let d_h = stream
5441                .clone_htod(&row_hessians)
5442                .expect("[bms_flex_row hvp_into_device parity] upload h must succeed on CUDA host");
5443            let d_m = stream.clone_htod(&marginal).expect(
5444                "[bms_flex_row hvp_into_device parity] upload marg must succeed on CUDA host",
5445            );
5446            let d_g = stream.clone_htod(&logslope).expect(
5447                "[bms_flex_row hvp_into_device parity] upload logslope must succeed on CUDA host",
5448            );
5449            let storage = DeviceResidentRowHess {
5450                neglog: stream
5451                    .alloc_zeros::<f64>(n)
5452                    .expect("[bms_flex_row hvp_into_device parity] alloc neglog"),
5453                grad: stream
5454                    .alloc_zeros::<f64>(n * r)
5455                    .expect("[bms_flex_row hvp_into_device parity] alloc grad"),
5456                hess: d_h,
5457                marginal_design: d_m,
5458                logslope_design: d_g,
5459                n,
5460                r,
5461                block: block.clone(),
5462                primary: primary.clone(),
5463
5464                bytes: ((n + n * r + n * r * r + n * p_m + n * p_g) * std::mem::size_of::<f64>())
5465                    as u64,
5466            };
5467
5468            // Host-out adapter (allocates its own d_out, syncs + downloads).
5469            let host_out_hvp = launch_bms_flex_row_hvp(&storage, &v)
5470                .expect("host-out HVP kernel must launch on CUDA host");
5471
5472            // Device-out adapter: caller owns d_v + d_out; the engine performs
5473            // no sync / DtoH, so we synchronize + download here.
5474            let d_v = stream
5475                .clone_htod(&v)
5476                .expect("upload direction for device-out HVP");
5477            let mut d_out = stream
5478                .alloc_zeros::<f64>(p_total)
5479                .expect("alloc device-out HVP output");
5480            launch_bms_flex_row_hvp_into_device(&storage, &d_v, &mut d_out)
5481                .expect("device-out HVP kernel must launch on CUDA host");
5482            stream
5483                .synchronize()
5484                .expect("synchronize after device-out HVP");
5485            let device_out_hvp = stream
5486                .clone_dtoh(&d_out)
5487                .expect("download device-out HVP output");
5488
5489            assert_eq!(device_out_hvp.len(), cpu_hvp.len());
5490            assert_eq!(device_out_hvp.len(), host_out_hvp.len());
5491            for i in 0..p_total {
5492                let diff = (cpu_hvp[i] - device_out_hvp[i]).abs();
5493                assert!(
5494                    diff <= 1e-10,
5495                    "device-out HVP[{i}] vs CPU: cpu={} gpu={} |Δ|={diff:.3e}",
5496                    cpu_hvp[i],
5497                    device_out_hvp[i]
5498                );
5499                // Both adapters share the engine; the only difference is the
5500                // copy-back path, so they must be bit-identical.
5501                let host_diff = (host_out_hvp[i] - device_out_hvp[i]).abs();
5502                assert!(
5503                    host_diff == 0.0,
5504                    "device-out vs host-out HVP[{i}]: host={} device={} |Δ|={host_diff:.3e}",
5505                    host_out_hvp[i],
5506                    device_out_hvp[i]
5507                );
5508            }
5509        }
5510    }
5511
5512    /// Block 9 Phase 2 parity gate at the shape specified by the
5513    /// charter task: `n = 64`, `r = 20`, `p_total = 44`. Splits
5514    /// `p_total` as `p_m = 14`, `p_g = 12`, `p_h = 10`, `p_w = 8` so
5515    /// `r = 2 + p_h + p_w = 20` and every primary block participates
5516    /// in both the device pullback and the reduce pass. Tolerance is
5517    /// `|Δ| ≤ 1e-8` per the task description (looser than the 1e-10
5518    /// hand-fixture parity, since accumulation order across HVP CTAs
5519    /// differs from the CPU oracle's row-major sum even with the
5520    /// deterministic reduction policy).
5521    ///
5522    /// Skips cleanly on non-Linux and no-CUDA hosts using the same
5523    /// convention as the hand-fixture parity above.
5524    #[test]
5525    pub(crate) fn bms_flex_row_hvp_kernel_matches_cpu_oracle_at_n64_r20_p44() {
5526        #[cfg(not(target_os = "linux"))]
5527        {
5528            eprintln!(
5529                "[bms_flex_row hvp parity n64_r20_p44] non-Linux host — \
5530                 skipping CUDA parity"
5531            );
5532        }
5533        #[cfg(target_os = "linux")]
5534        {
5535            if cuda_runtime_for_test("bms_flex_row hvp parity n64_r20_p44").is_none() {
5536                return;
5537            }
5538            let n = 64_usize;
5539            let p_m = 14_usize;
5540            let p_g = 12_usize;
5541            let p_h_dim = 10_usize;
5542            let p_w_dim = 8_usize;
5543            let r = 2 + p_h_dim + p_w_dim;
5544            assert_eq!(r, 20);
5545            let p_total = p_m + p_g + p_h_dim + p_w_dim;
5546            assert_eq!(p_total, 44);
5547            let block = BmsFlexBlockLayout {
5548                p_m,
5549                p_g,
5550                h: Some(p_m + p_g..p_m + p_g + p_h_dim),
5551                w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
5552                p_total,
5553            };
5554            let primary = BmsFlexPrimaryLayout {
5555                h: Some(2..2 + p_h_dim),
5556                w: Some(2 + p_h_dim..2 + p_h_dim + p_w_dim),
5557                r,
5558            };
5559
5560            // Deterministic symmetric per-row Hessians + designs +
5561            // direction. Same scrambling family as
5562            // `row_hessian_ops::tests::make_fixture` so any regression
5563            // surfaces consistently across the host-pinned and
5564            // device-resident parity tests.
5565            let mut row_hessians = vec![0.0_f64; n * r * r];
5566            for row in 0..n {
5567                let base = row * r * r;
5568                for u in 0..r {
5569                    for v in 0..r {
5570                        let seed = (row as f64) * 0.137 + (u as f64) * 1.901 + (v as f64) * 0.317;
5571                        let a = (seed.sin() * 1.7 + (seed * 0.5).cos() * 0.9) * 0.5;
5572                        row_hessians[base + u * r + v] = a;
5573                    }
5574                }
5575                for u in 0..r {
5576                    for v in (u + 1)..r {
5577                        let upper = row_hessians[base + u * r + v];
5578                        let lower = row_hessians[base + v * r + u];
5579                        let sym = 0.5 * (upper + lower);
5580                        row_hessians[base + u * r + v] = sym;
5581                        row_hessians[base + v * r + u] = sym;
5582                    }
5583                    row_hessians[base + u * r + u] += r as f64;
5584                }
5585            }
5586            let mut marginal = vec![0.0_f64; n * p_m];
5587            for row in 0..n {
5588                for j in 0..p_m {
5589                    let seed = (row as f64) * 0.073 + (j as f64) * 0.211 + 0.4;
5590                    marginal[row * p_m + j] = seed.sin() * 0.8 - (seed * 0.7).cos() * 0.3;
5591                }
5592            }
5593            let mut logslope = vec![0.0_f64; n * p_g];
5594            for row in 0..n {
5595                for j in 0..p_g {
5596                    let seed = (row as f64) * 0.091 + (j as f64) * 0.179 - 0.2;
5597                    logslope[row * p_g + j] = seed.cos() * 0.7 + (seed * 0.3).sin() * 0.25;
5598                }
5599            }
5600            let v: Vec<f64> = (0..p_total)
5601                .map(|i| {
5602                    let seed = (i as f64) * 0.157 + 0.6;
5603                    seed.sin() * 0.55 + (seed * 0.4).cos() * 0.35
5604                })
5605                .collect();
5606
5607            let cpu_hvp = cpu_oracle_bms_flex_row_hvp(
5608                &row_hessians,
5609                &marginal,
5610                &logslope,
5611                &block,
5612                &primary,
5613                n,
5614                &v,
5615            );
5616            let cpu_diag = cpu_oracle_bms_flex_row_diagonal(
5617                &row_hessians,
5618                &marginal,
5619                &logslope,
5620                &block,
5621                &primary,
5622                n,
5623            );
5624
5625            let backend = match HvpKernelBackend::probe() {
5626                Ok(b) => b,
5627                Err(err) => {
5628                    eprintln!(
5629                        "[bms_flex_row hvp parity n64_r20_p44] backend probe \
5630                         failed: {err}"
5631                    );
5632                    return;
5633                }
5634            };
5635            let stream = backend.stream.clone();
5636            let d_h = match stream.clone_htod(&row_hessians) {
5637                Ok(s) => s,
5638                Err(err) => {
5639                    eprintln!(
5640                        "[bms_flex_row hvp parity n64_r20_p44] upload h \
5641                         failed: {err}"
5642                    );
5643                    return;
5644                }
5645            };
5646            let d_m = match stream.clone_htod(&marginal) {
5647                Ok(s) => s,
5648                Err(err) => {
5649                    eprintln!(
5650                        "[bms_flex_row hvp parity n64_r20_p44] upload marg \
5651                         failed: {err}"
5652                    );
5653                    return;
5654                }
5655            };
5656            let d_g = match stream.clone_htod(&logslope) {
5657                Ok(s) => s,
5658                Err(err) => {
5659                    eprintln!(
5660                        "[bms_flex_row hvp parity n64_r20_p44] upload logslope \
5661                         failed: {err}"
5662                    );
5663                    return;
5664                }
5665            };
5666            let storage = DeviceResidentRowHess {
5667                neglog: stream
5668                    .alloc_zeros::<f64>(n)
5669                    .expect("[bms_flex_row hvp parity n64_r20_p44] alloc neglog"),
5670                grad: stream
5671                    .alloc_zeros::<f64>(n * r)
5672                    .expect("[bms_flex_row hvp parity n64_r20_p44] alloc grad"),
5673                hess: d_h,
5674                marginal_design: d_m,
5675                logslope_design: d_g,
5676                n,
5677                r,
5678                block: block.clone(),
5679                primary: primary.clone(),
5680
5681                bytes: ((n + n * r + n * r * r + n * p_m + n * p_g) * std::mem::size_of::<f64>())
5682                    as u64,
5683            };
5684            let gpu_hvp = launch_bms_flex_row_hvp(&storage, &v)
5685                .expect("HVP kernel must launch on CUDA host at n64/r20/p44");
5686            let gpu_diag = launch_bms_flex_row_diagonal(&storage)
5687                .expect("diagonal kernel must launch on CUDA host at n64/r20/p44");
5688            assert_eq!(gpu_hvp.len(), cpu_hvp.len());
5689            assert_eq!(gpu_diag.len(), cpu_diag.len());
5690            for i in 0..p_total {
5691                let diff = (cpu_hvp[i] - gpu_hvp[i]).abs();
5692                assert!(
5693                    diff <= 1e-8,
5694                    "n64_r20_p44 HVP[{i}]: cpu={} gpu={} |Δ|={diff:.3e}",
5695                    cpu_hvp[i],
5696                    gpu_hvp[i]
5697                );
5698                let ddiff = (cpu_diag[i] - gpu_diag[i]).abs();
5699                assert!(
5700                    ddiff <= 1e-8,
5701                    "n64_r20_p44 diag[{i}]: cpu={} gpu={} |Δ|={ddiff:.3e}",
5702                    cpu_diag[i],
5703                    gpu_diag[i]
5704                );
5705            }
5706        }
5707    }
5708
5709    /// Block 9 Phase 6 — small-fixture parity for the dense-block kernel
5710    /// against the host-side P_i pullback oracle.
5711    /// Verifies bit-equality (modulo reduction-order f.p. noise) between
5712    /// the device-resident dense build and the host accumulator over the
5713    /// same per-row Hessian + designs + P_i pullback.
5714    #[test]
5715    pub(crate) fn bms_flex_row_dense_block_kernel_matches_cpu_pullback() {
5716        #[cfg(not(target_os = "linux"))]
5717        {
5718            eprintln!("[bms_flex_row dense_block parity] non-Linux host — skipping CUDA parity");
5719        }
5720        #[cfg(target_os = "linux")]
5721        {
5722            if cuda_runtime_for_test("bms_flex_row dense_block parity").is_none() {
5723                return;
5724            }
5725            // Small fixture: n=24, r=8 (2 + 3 + 3), p_total=18 (4+4+3+3).
5726            // Keeps the CPU pullback fast while still exercising every
5727            // primary slot (q, g, h, w).
5728            let n = 24_usize;
5729            let p_m = 4_usize;
5730            let p_g = 4_usize;
5731            let p_h_dim = 3_usize;
5732            let p_w_dim = 3_usize;
5733            let r = 2 + p_h_dim + p_w_dim;
5734            let p_total = p_m + p_g + p_h_dim + p_w_dim;
5735            let block = BmsFlexBlockLayout {
5736                p_m,
5737                p_g,
5738                h: Some(p_m + p_g..p_m + p_g + p_h_dim),
5739                w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
5740                p_total,
5741            };
5742            let primary = BmsFlexPrimaryLayout {
5743                h: Some(2..2 + p_h_dim),
5744                w: Some(2 + p_h_dim..2 + p_h_dim + p_w_dim),
5745                r,
5746            };
5747
5748            let mut row_hessians = vec![0.0_f64; n * r * r];
5749            for row in 0..n {
5750                let base = row * r * r;
5751                for u in 0..r {
5752                    for v in 0..r {
5753                        let seed = (row as f64) * 0.21 + (u as f64) * 1.13 + (v as f64) * 0.47;
5754                        let a = (seed.sin() * 1.4 + (seed * 0.6).cos() * 0.7) * 0.5;
5755                        row_hessians[base + u * r + v] = a;
5756                    }
5757                }
5758                for u in 0..r {
5759                    for v in (u + 1)..r {
5760                        let upper = row_hessians[base + u * r + v];
5761                        let lower = row_hessians[base + v * r + u];
5762                        let sym = 0.5 * (upper + lower);
5763                        row_hessians[base + u * r + v] = sym;
5764                        row_hessians[base + v * r + u] = sym;
5765                    }
5766                    row_hessians[base + u * r + u] += r as f64;
5767                }
5768            }
5769            let mut marginal = vec![0.0_f64; n * p_m];
5770            for row in 0..n {
5771                for j in 0..p_m {
5772                    let seed = (row as f64) * 0.083 + (j as f64) * 0.171 + 0.31;
5773                    marginal[row * p_m + j] = seed.sin() * 0.7 - (seed * 0.5).cos() * 0.25;
5774                }
5775            }
5776            let mut logslope = vec![0.0_f64; n * p_g];
5777            for row in 0..n {
5778                for j in 0..p_g {
5779                    let seed = (row as f64) * 0.097 + (j as f64) * 0.143 - 0.15;
5780                    logslope[row * p_g + j] = seed.cos() * 0.65 + (seed * 0.4).sin() * 0.2;
5781                }
5782            }
5783
5784            // CPU oracle — same pullback math the device kernel mirrors.
5785            let h_block_start = block.h.as_ref().map(|r| r.start).unwrap_or(0);
5786            let h_block_len = block.h.as_ref().map(|r| r.len()).unwrap_or(0);
5787            let w_block_start = block.w.as_ref().map(|r| r.start).unwrap_or(0);
5788            let w_block_len = block.w.as_ref().map(|r| r.len()).unwrap_or(0);
5789            let h_primary_start = primary.h.as_ref().map(|r| r.start).unwrap_or(0);
5790            let w_primary_start = primary.w.as_ref().map(|r| r.start).unwrap_or(0);
5791            let mut h_cpu = vec![0.0_f64; p_total * p_total];
5792            for row in 0..n {
5793                let mrow = &marginal[row * p_m..(row + 1) * p_m];
5794                let grow = &logslope[row * p_g..(row + 1) * p_g];
5795                let hrow = &row_hessians[row * r * r..(row + 1) * r * r];
5796                // Build per-row phi (r length-p_total vectors).
5797                let mut phi = vec![vec![0.0_f64; p_total]; r];
5798                for k in 0..p_m {
5799                    phi[0][k] = mrow[k];
5800                }
5801                for k in 0..p_g {
5802                    phi[1][p_m + k] = grow[k];
5803                }
5804                for k in 0..h_block_len {
5805                    phi[h_primary_start + k][h_block_start + k] = 1.0;
5806                }
5807                for k in 0..w_block_len {
5808                    phi[w_primary_start + k][w_block_start + k] = 1.0;
5809                }
5810                for u in 0..r {
5811                    for v in 0..r {
5812                        let huv = hrow[u * r + v];
5813                        if huv == 0.0 {
5814                            continue;
5815                        }
5816                        for m in 0..p_total {
5817                            let pm = phi[u][m];
5818                            if pm == 0.0 {
5819                                continue;
5820                            }
5821                            let scaled = huv * pm;
5822                            for nn in 0..p_total {
5823                                h_cpu[m * p_total + nn] += scaled * phi[v][nn];
5824                            }
5825                        }
5826                    }
5827                }
5828            }
5829
5830            // Build a transient device-resident storage and launch the
5831            // dense-block kernel.
5832            // Past the lossless Auto-resolution gate: probe/upload failures are
5833            // real device faults on a CUDA host — fail loud (device-PCG class).
5834            let backend = HvpKernelBackend::probe().expect(
5835                "[bms_flex_row dense_block parity] backend probe must succeed on CUDA host",
5836            );
5837            let stream = backend.stream.clone();
5838            let d_h = stream
5839                .clone_htod(&row_hessians)
5840                .expect("[bms_flex_row dense_block parity] upload h must succeed on CUDA host");
5841            let d_m = stream
5842                .clone_htod(&marginal)
5843                .expect("[bms_flex_row dense_block parity] upload marg must succeed on CUDA host");
5844            let d_g = stream.clone_htod(&logslope).expect(
5845                "[bms_flex_row dense_block parity] upload logslope must succeed on CUDA host",
5846            );
5847            let storage = DeviceResidentRowHess {
5848                neglog: stream
5849                    .alloc_zeros::<f64>(n)
5850                    .expect("[bms_flex_row dense_block parity] alloc neglog"),
5851                grad: stream
5852                    .alloc_zeros::<f64>(n * r)
5853                    .expect("[bms_flex_row dense_block parity] alloc grad"),
5854                hess: d_h,
5855                marginal_design: d_m,
5856                logslope_design: d_g,
5857                n,
5858                r,
5859                block: block.clone(),
5860                primary: primary.clone(),
5861
5862                bytes: ((n + n * r + n * r * r + n * p_m + n * p_g) * std::mem::size_of::<f64>())
5863                    as u64,
5864            };
5865            let h_gpu = launch_bms_flex_row_dense_block(&storage)
5866                .expect("dense_block kernel must launch on CUDA host");
5867            assert_eq!(h_gpu.len(), p_total * p_total);
5868
5869            // Compare entry-by-entry with a tolerance that absorbs
5870            // reduction-order f.p. noise from the CTA chunk sum.
5871            let mut max_abs = 0.0_f64;
5872            for i in 0..p_total {
5873                for j in 0..p_total {
5874                    let a = h_cpu[i * p_total + j];
5875                    let b = h_gpu[i * p_total + j];
5876                    let diff = (a - b).abs();
5877                    if diff > max_abs {
5878                        max_abs = diff;
5879                    }
5880                    assert!(
5881                        diff <= 1e-9 * a.abs().max(b.abs()).max(1.0),
5882                        "dense_block[{i},{j}]: cpu={a} gpu={b} |Δ|={diff:.3e}"
5883                    );
5884                }
5885            }
5886            eprintln!(
5887                "[bms_flex_row dense_block parity] n={n} r={r} p={p_total}: max|Δ|={max_abs:.3e}"
5888            );
5889        }
5890    }
5891
5892    #[test]
5893    pub(crate) fn bms_flex_row_dense_hvp_materialization_matches_cpu_above_block_cap_932() {
5894        if cuda_runtime_for_test("bms_flex_row dense HVP parity").is_none() {
5895            return;
5896        }
5897        let n = 1_usize;
5898        let r = 2_usize;
5899        let p_m = 37_usize;
5900        let p_g = 36_usize;
5901        let p_total = p_m + p_g;
5902        assert_eq!(p_total, DENSE_BLOCK_MAX_P + 1);
5903        let block = BmsFlexBlockLayout {
5904            p_m,
5905            p_g,
5906            h: None,
5907            w: None,
5908            p_total,
5909        };
5910        let primary = BmsFlexPrimaryLayout {
5911            h: None,
5912            w: None,
5913            r,
5914        };
5915        let row_hessians = vec![2.5_f64, -0.75, -0.75, 1.25];
5916        let marginal = (0..p_m)
5917            .map(|column| 0.15 + (column as f64 * 0.17).sin())
5918            .collect::<Vec<_>>();
5919        let logslope = (0..p_g)
5920            .map(|column| -0.2 + (column as f64 * 0.11).cos())
5921            .collect::<Vec<_>>();
5922        let mut expected = vec![0.0_f64; p_total * p_total];
5923        for row in 0..p_total {
5924            for column in 0..p_total {
5925                expected[row * p_total + column] = match (row < p_m, column < p_m) {
5926                    (true, true) => row_hessians[0] * marginal[row] * marginal[column],
5927                    (true, false) => row_hessians[1] * marginal[row] * logslope[column - p_m],
5928                    (false, true) => row_hessians[2] * logslope[row - p_m] * marginal[column],
5929                    (false, false) => {
5930                        row_hessians[3] * logslope[row - p_m] * logslope[column - p_m]
5931                    }
5932                };
5933            }
5934        }
5935
5936        let backend = HvpKernelBackend::probe()
5937            .expect("[bms_flex_row dense HVP parity] backend probe must succeed");
5938        let stream = backend.stream.clone();
5939        let storage = DeviceResidentRowHess {
5940            neglog: stream
5941                .alloc_zeros::<f64>(n)
5942                .expect("dense HVP parity neglog allocation"),
5943            grad: stream
5944                .alloc_zeros::<f64>(n * r)
5945                .expect("dense HVP parity grad allocation"),
5946            hess: stream
5947                .clone_htod(&row_hessians)
5948                .expect("dense HVP parity hessian upload"),
5949            marginal_design: stream
5950                .clone_htod(&marginal)
5951                .expect("dense HVP parity marginal upload"),
5952            logslope_design: stream
5953                .clone_htod(&logslope)
5954                .expect("dense HVP parity logslope upload"),
5955            n,
5956            r,
5957            block,
5958            primary,
5959            bytes: ((n + n * r + n * r * r + n * p_m + n * p_g) * std::mem::size_of::<f64>())
5960                as u64,
5961        };
5962        let actual = launch_bms_flex_row_dense(&storage)
5963            .expect("wide dense HVP materialization must stay on CUDA");
5964        assert_eq!(actual.len(), expected.len());
5965        for (index, (&actual, &expected)) in actual.iter().zip(&expected).enumerate() {
5966            let tolerance = 1.0e-10 * actual.abs().max(expected.abs()).max(1.0);
5967            assert!(
5968                (actual - expected).abs() <= tolerance,
5969                "wide dense entry {index}: CUDA={actual:.17e} CPU={expected:.17e} tolerance={tolerance:.3e}"
5970            );
5971        }
5972    }
5973
5974    /// Block 9 final hill-climb gate — GPU HVP must be at least 5× faster
5975    /// than a Rayon-parallel CPU HVP at large-scale shape (n=195_000, r=20,
5976    /// p_total=44). This is the charter pass/fail metric for whether the
5977    /// device-resident row-Hessian path is a real perf win for the
5978    /// production marginal-slope fit.
5979    ///
5980    /// Methodology:
5981    ///   * Build the same deterministic fixture as the parity tests.
5982    ///   * GPU: median of `iters` `launch_bms_flex_row_hvp` wall-times
5983    ///     after `warmup` warm-up launches (kernel compile + L2 prime).
5984    ///   * CPU: median of `iters` `cpu_oracle_bms_flex_row_hvp` wall-times,
5985    ///     parallelised over rows via Rayon — this mirrors the actual
5986    ///     production CPU path in
5987    ///     `exact_newton_joint_hessian_matvec_from_cache` (which uses
5988    ///     `ROW_CHUNK_SIZE` chunked `into_par_iter()` for the same
5989    ///     contraction).
5990    ///   * The gate is the calibrated policy's decision that this row count
5991    ///     belongs on the device; cpu_median / gpu_median is a printed perf
5992    ///     record. It asserted `ratio >= 5` until #2487.
5993    ///
5994    /// Skips on non-Linux / no-CUDA hosts.
5995    #[test]
5996    pub(crate) fn bms_flex_row_hvp_dispatch_worthiness_at_large_scale() {
5997        #[cfg(not(target_os = "linux"))]
5998        {
5999            eprintln!("[bms_flex_row hvp hill-climb] non-Linux host — skipping V100 perf gate");
6000        }
6001        #[cfg(target_os = "linux")]
6002        {
6003            let Some(runtime) = cuda_runtime_for_test("bms_flex_row hvp hill-climb") else {
6004                return;
6005            };
6006            let n = 195_000_usize;
6007            let p_m = 14_usize;
6008            let p_g = 12_usize;
6009            let p_h_dim = 10_usize;
6010            let p_w_dim = 8_usize;
6011            let r = 2 + p_h_dim + p_w_dim;
6012            let p_total = p_m + p_g + p_h_dim + p_w_dim;
6013            let block = BmsFlexBlockLayout {
6014                p_m,
6015                p_g,
6016                h: Some(p_m + p_g..p_m + p_g + p_h_dim),
6017                w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
6018                p_total,
6019            };
6020            let primary = BmsFlexPrimaryLayout {
6021                h: Some(2..2 + p_h_dim),
6022                w: Some(2 + p_h_dim..2 + p_h_dim + p_w_dim),
6023                r,
6024            };
6025
6026            // Same deterministic fixture as the Phase 4 large-scale benchmark.
6027            let mut row_hessians = vec![0.0_f64; n * r * r];
6028            for row in 0..n {
6029                let base = row * r * r;
6030                for u in 0..r {
6031                    for vv in 0..r {
6032                        let seed = (row as f64) * 0.137 + (u as f64) * 1.901 + (vv as f64) * 0.317;
6033                        let a = (seed.sin() * 1.7 + (seed * 0.5).cos() * 0.9) * 0.5;
6034                        row_hessians[base + u * r + vv] = a;
6035                    }
6036                }
6037                for u in 0..r {
6038                    for vv in (u + 1)..r {
6039                        let upper = row_hessians[base + u * r + vv];
6040                        let lower = row_hessians[base + vv * r + u];
6041                        let sym = 0.5 * (upper + lower);
6042                        row_hessians[base + u * r + vv] = sym;
6043                        row_hessians[base + vv * r + u] = sym;
6044                    }
6045                    row_hessians[base + u * r + u] += r as f64;
6046                }
6047            }
6048            let mut marginal = vec![0.0_f64; n * p_m];
6049            for row in 0..n {
6050                for j in 0..p_m {
6051                    let seed = (row as f64) * 0.073 + (j as f64) * 0.211 + 0.4;
6052                    marginal[row * p_m + j] = seed.sin() * 0.8 - (seed * 0.7).cos() * 0.3;
6053                }
6054            }
6055            let mut logslope = vec![0.0_f64; n * p_g];
6056            for row in 0..n {
6057                for j in 0..p_g {
6058                    let seed = (row as f64) * 0.091 + (j as f64) * 0.179 - 0.2;
6059                    logslope[row * p_g + j] = seed.cos() * 0.7 + (seed * 0.3).sin() * 0.25;
6060                }
6061            }
6062            let v: Vec<f64> = (0..p_total)
6063                .map(|i| {
6064                    let seed = (i as f64) * 0.157 + 0.6;
6065                    seed.sin() * 0.55 + (seed * 0.4).cos() * 0.35
6066                })
6067                .collect();
6068
6069            // ── GPU side: upload once, time HVP launches ─────────────
6070            let backend = match HvpKernelBackend::probe() {
6071                Ok(b) => b,
6072                Err(err) => {
6073                    eprintln!("[bms_flex_row hvp hill-climb] backend probe failed: {err}");
6074                    return;
6075                }
6076            };
6077            let stream = backend.stream.clone();
6078            let d_h = match stream.clone_htod(&row_hessians) {
6079                Ok(s) => s,
6080                Err(err) => {
6081                    eprintln!("[bms_flex_row hvp hill-climb] upload h failed (likely OOM): {err}");
6082                    return;
6083                }
6084            };
6085            let d_m = match stream.clone_htod(&marginal) {
6086                Ok(s) => s,
6087                Err(err) => {
6088                    eprintln!("[bms_flex_row hvp hill-climb] upload marg failed: {err}");
6089                    return;
6090                }
6091            };
6092            let d_g = match stream.clone_htod(&logslope) {
6093                Ok(s) => s,
6094                Err(err) => {
6095                    eprintln!("[bms_flex_row hvp hill-climb] upload logslope failed: {err}");
6096                    return;
6097                }
6098            };
6099            let storage = DeviceResidentRowHess {
6100                neglog: stream
6101                    .alloc_zeros::<f64>(n)
6102                    .expect("[bms_flex_row hvp hill-climb] alloc neglog"),
6103                grad: stream
6104                    .alloc_zeros::<f64>(n * r)
6105                    .expect("[bms_flex_row hvp hill-climb] alloc grad"),
6106                hess: d_h,
6107                marginal_design: d_m,
6108                logslope_design: d_g,
6109                n,
6110                r,
6111                block: block.clone(),
6112                primary: primary.clone(),
6113
6114                bytes: ((n + n * r + n * r * r + n * p_m + n * p_g) * std::mem::size_of::<f64>())
6115                    as u64,
6116            };
6117            let warmup: usize = 3;
6118            let iters: usize = 15;
6119            for _ in 0..warmup {
6120                let out =
6121                    launch_bms_flex_row_hvp(&storage, &v).expect("warmup GPU HVP must launch");
6122                assert_eq!(out.len(), p_total);
6123            }
6124            let mut gpu_us: Vec<u128> = Vec::with_capacity(iters);
6125            for _ in 0..iters {
6126                let t0 = std::time::Instant::now();
6127                let out = launch_bms_flex_row_hvp(&storage, &v).expect("GPU HVP must launch");
6128                gpu_us.push(t0.elapsed().as_micros());
6129                assert_eq!(out.len(), p_total);
6130            }
6131            gpu_us.sort_unstable();
6132            let gpu_median = gpu_us[iters / 2];
6133
6134            // ── CPU side: chunked Rayon HVP over rows, mirroring the
6135            //    production `exact_newton_joint_hessian_matvec_from_cache`
6136            //    parallelisation pattern (ROW_CHUNK_SIZE-row chunks,
6137            //    try_fold + try_reduce). The per-chunk worker calls the
6138            //    single-threaded oracle on its row slice.
6139            const CHUNK_ROWS: usize = 4096;
6140            let cpu_hvp_parallel = || -> Vec<f64> {
6141                let nchunks = n.div_ceil(CHUNK_ROWS);
6142                gam_linalg::pairwise_reduce::par_deterministic_block_fold(
6143                    nchunks,
6144                    |ci_range| {
6145                        let mut acc = vec![0.0_f64; p_total];
6146                        for ci in ci_range {
6147                            let lo = ci * CHUNK_ROWS;
6148                            let hi = (lo + CHUNK_ROWS).min(n);
6149                            let m = hi - lo;
6150                            let partial = cpu_oracle_bms_flex_row_hvp(
6151                                &row_hessians[lo * r * r..hi * r * r],
6152                                &marginal[lo * p_m..hi * p_m],
6153                                &logslope[lo * p_g..hi * p_g],
6154                                &block,
6155                                &primary,
6156                                m,
6157                                &v,
6158                            );
6159                            for (a, &p) in acc.iter_mut().zip(partial.iter()) {
6160                                *a += p;
6161                            }
6162                        }
6163                        acc
6164                    },
6165                    |mut a, b| {
6166                        for (ax, bx) in a.iter_mut().zip(b.iter()) {
6167                            *ax += *bx;
6168                        }
6169                        a
6170                    },
6171                )
6172                .unwrap_or_else(|| vec![0.0_f64; p_total])
6173            };
6174            // Warmup once to populate L3 / steady-state Rayon thread pool.
6175            let warm = cpu_hvp_parallel();
6176            assert_eq!(warm.len(), p_total);
6177            let mut cpu_us: Vec<u128> = Vec::with_capacity(iters);
6178            for _ in 0..iters {
6179                let t0 = std::time::Instant::now();
6180                let out = cpu_hvp_parallel();
6181                cpu_us.push(t0.elapsed().as_micros());
6182                assert_eq!(out.len(), p_total);
6183            }
6184            cpu_us.sort_unstable();
6185            let cpu_median = cpu_us[iters / 2];
6186
6187            let speedup = (cpu_median as f64) / (gpu_median.max(1) as f64);
6188            eprintln!(
6189                "[bms_flex_row hvp hill-climb] large-scale n={n} r={r} p={p_total}: \
6190                 cpu_median={cpu_median}us gpu_median={gpu_median}us \
6191                 speedup={speedup:.2}× (perf record; the gate is the policy decision)"
6192            );
6193            assert_row_batch_dispatch_worthy(
6194                "large-scale HVP dispatch-worthiness gate",
6195                runtime.policy(),
6196                n,
6197            );
6198        }
6199    }
6200
6201    /// Companion to the HVP hill-climb: GPU dense-block build must be at
6202    /// least 10× faster than a Rayon-parallel CPU dense build at large-scale
6203    /// shape. The dense build is `O(n * r² * p_total)` work for both
6204    /// paths so the ratio is well-defined.
6205    #[test]
6206    pub(crate) fn bms_flex_row_dense_block_dispatch_worthiness_at_large_scale() {
6207        #[cfg(not(target_os = "linux"))]
6208        {
6209            eprintln!(
6210                "[bms_flex_row dense_block hill-climb] non-Linux host — skipping V100 perf gate"
6211            );
6212        }
6213        #[cfg(target_os = "linux")]
6214        {
6215            let Some(runtime) = cuda_runtime_for_test("bms_flex_row dense_block hill-climb")
6216            else {
6217                return;
6218            };
6219            let n = 195_000_usize;
6220            let p_m = 14_usize;
6221            let p_g = 12_usize;
6222            let p_h_dim = 10_usize;
6223            let p_w_dim = 8_usize;
6224            let r = 2 + p_h_dim + p_w_dim;
6225            let p_total = p_m + p_g + p_h_dim + p_w_dim;
6226            let block = BmsFlexBlockLayout {
6227                p_m,
6228                p_g,
6229                h: Some(p_m + p_g..p_m + p_g + p_h_dim),
6230                w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
6231                p_total,
6232            };
6233            let primary = BmsFlexPrimaryLayout {
6234                h: Some(2..2 + p_h_dim),
6235                w: Some(2 + p_h_dim..2 + p_h_dim + p_w_dim),
6236                r,
6237            };
6238
6239            // Reuse the same large-scale fixture recipe.
6240            let mut row_hessians = vec![0.0_f64; n * r * r];
6241            for row in 0..n {
6242                let base = row * r * r;
6243                for u in 0..r {
6244                    for vv in 0..r {
6245                        let seed = (row as f64) * 0.137 + (u as f64) * 1.901 + (vv as f64) * 0.317;
6246                        let a = (seed.sin() * 1.7 + (seed * 0.5).cos() * 0.9) * 0.5;
6247                        row_hessians[base + u * r + vv] = a;
6248                    }
6249                }
6250                for u in 0..r {
6251                    for vv in (u + 1)..r {
6252                        let upper = row_hessians[base + u * r + vv];
6253                        let lower = row_hessians[base + vv * r + u];
6254                        let sym = 0.5 * (upper + lower);
6255                        row_hessians[base + u * r + vv] = sym;
6256                        row_hessians[base + vv * r + u] = sym;
6257                    }
6258                    row_hessians[base + u * r + u] += r as f64;
6259                }
6260            }
6261            let mut marginal = vec![0.0_f64; n * p_m];
6262            for row in 0..n {
6263                for j in 0..p_m {
6264                    let seed = (row as f64) * 0.073 + (j as f64) * 0.211 + 0.4;
6265                    marginal[row * p_m + j] = seed.sin() * 0.8 - (seed * 0.7).cos() * 0.3;
6266                }
6267            }
6268            let mut logslope = vec![0.0_f64; n * p_g];
6269            for row in 0..n {
6270                for j in 0..p_g {
6271                    let seed = (row as f64) * 0.091 + (j as f64) * 0.179 - 0.2;
6272                    logslope[row * p_g + j] = seed.cos() * 0.7 + (seed * 0.3).sin() * 0.25;
6273                }
6274            }
6275
6276            // GPU dense_block kernel rejects p_total > DENSE_BLOCK_MAX_P
6277            // (72 at V100 48 KiB/block). LargeScale's p_total = 44 fits.
6278            if p_total > DENSE_BLOCK_MAX_P {
6279                eprintln!(
6280                    "[bms_flex_row dense_block hill-climb] p_total={p_total} > MAX={DENSE_BLOCK_MAX_P}, skipping"
6281                );
6282                return;
6283            }
6284            let backend = match HvpKernelBackend::probe() {
6285                Ok(b) => b,
6286                Err(err) => {
6287                    eprintln!("[bms_flex_row dense_block hill-climb] backend probe failed: {err}");
6288                    return;
6289                }
6290            };
6291            let stream = backend.stream.clone();
6292            let d_h = match stream.clone_htod(&row_hessians) {
6293                Ok(s) => s,
6294                Err(err) => {
6295                    eprintln!("[bms_flex_row dense_block hill-climb] upload h failed: {err}");
6296                    return;
6297                }
6298            };
6299            let d_m = match stream.clone_htod(&marginal) {
6300                Ok(s) => s,
6301                Err(err) => {
6302                    eprintln!("[bms_flex_row dense_block hill-climb] upload marg failed: {err}");
6303                    return;
6304                }
6305            };
6306            let d_g = match stream.clone_htod(&logslope) {
6307                Ok(s) => s,
6308                Err(err) => {
6309                    eprintln!(
6310                        "[bms_flex_row dense_block hill-climb] upload logslope failed: {err}"
6311                    );
6312                    return;
6313                }
6314            };
6315            let storage = DeviceResidentRowHess {
6316                neglog: stream
6317                    .alloc_zeros::<f64>(n)
6318                    .expect("[bms_flex_row dense_block hill-climb] alloc neglog"),
6319                grad: stream
6320                    .alloc_zeros::<f64>(n * r)
6321                    .expect("[bms_flex_row dense_block hill-climb] alloc grad"),
6322                hess: d_h,
6323                marginal_design: d_m,
6324                logslope_design: d_g,
6325                n,
6326                r,
6327                block: block.clone(),
6328                primary: primary.clone(),
6329
6330                bytes: ((n + n * r + n * r * r + n * p_m + n * p_g) * std::mem::size_of::<f64>())
6331                    as u64,
6332            };
6333            // Warmup + 5-iter median (dense build is heavier than HVP).
6334            let warmup: usize = 2;
6335            let iters: usize = 5;
6336            for _ in 0..warmup {
6337                let out = launch_bms_flex_row_dense_block(&storage)
6338                    .expect("warmup GPU dense_block must launch");
6339                assert_eq!(out.len(), p_total * p_total);
6340            }
6341            let mut gpu_us: Vec<u128> = Vec::with_capacity(iters);
6342            for _ in 0..iters {
6343                let t0 = std::time::Instant::now();
6344                let out =
6345                    launch_bms_flex_row_dense_block(&storage).expect("GPU dense_block must launch");
6346                gpu_us.push(t0.elapsed().as_micros());
6347                assert_eq!(out.len(), p_total * p_total);
6348            }
6349            gpu_us.sort_unstable();
6350            let gpu_median = gpu_us[iters / 2];
6351
6352            // CPU side: chunked Rayon dense build over rows. Each chunk
6353            // builds a `[p_total, p_total]` partial then we reduce-add.
6354            const CHUNK_ROWS: usize = 2048;
6355            let h_block_start = block.h.as_ref().map(|r| r.start).unwrap_or(0);
6356            let h_block_len = block.h.as_ref().map(|r| r.len()).unwrap_or(0);
6357            let w_block_start = block.w.as_ref().map(|r| r.start).unwrap_or(0);
6358            let w_block_len = block.w.as_ref().map(|r| r.len()).unwrap_or(0);
6359            let h_primary_start = primary.h.as_ref().map(|r| r.start).unwrap_or(0);
6360            let w_primary_start = primary.w.as_ref().map(|r| r.start).unwrap_or(0);
6361            let cpu_build_parallel = || -> Vec<f64> {
6362                let nchunks = n.div_ceil(CHUNK_ROWS);
6363                gam_linalg::pairwise_reduce::par_deterministic_block_fold(
6364                    nchunks,
6365                    |ci_range| {
6366                        let mut acc = vec![0.0_f64; p_total * p_total];
6367                        let mut phi: Vec<Vec<f64>> = vec![vec![0.0_f64; p_total]; r];
6368                        for ci in ci_range {
6369                            let lo = ci * CHUNK_ROWS;
6370                            let hi = (lo + CHUNK_ROWS).min(n);
6371                            for row in lo..hi {
6372                                for col in phi.iter_mut() {
6373                                    col.iter_mut().for_each(|v| *v = 0.0);
6374                                }
6375                                let mrow = &marginal[row * p_m..(row + 1) * p_m];
6376                                let grow = &logslope[row * p_g..(row + 1) * p_g];
6377                                for k in 0..p_m {
6378                                    phi[0][k] = mrow[k];
6379                                }
6380                                for k in 0..p_g {
6381                                    phi[1][p_m + k] = grow[k];
6382                                }
6383                                for k in 0..h_block_len {
6384                                    phi[h_primary_start + k][h_block_start + k] = 1.0;
6385                                }
6386                                for k in 0..w_block_len {
6387                                    phi[w_primary_start + k][w_block_start + k] = 1.0;
6388                                }
6389                                let hrow = &row_hessians[row * r * r..(row + 1) * r * r];
6390                                for u in 0..r {
6391                                    for v_idx in 0..r {
6392                                        let huv = hrow[u * r + v_idx];
6393                                        if huv == 0.0 {
6394                                            continue;
6395                                        }
6396                                        for m in 0..p_total {
6397                                            let pm = phi[u][m];
6398                                            if pm == 0.0 {
6399                                                continue;
6400                                            }
6401                                            let scaled = huv * pm;
6402                                            for nn in 0..p_total {
6403                                                acc[m * p_total + nn] += scaled * phi[v_idx][nn];
6404                                            }
6405                                        }
6406                                    }
6407                                }
6408                            }
6409                        }
6410                        acc
6411                    },
6412                    |mut a, b| {
6413                        for (ax, bx) in a.iter_mut().zip(b.iter()) {
6414                            *ax += *bx;
6415                        }
6416                        a
6417                    },
6418                )
6419                .unwrap_or_else(|| vec![0.0_f64; p_total * p_total])
6420            };
6421            let warm_cpu = cpu_build_parallel();
6422            assert_eq!(warm_cpu.len(), p_total * p_total);
6423            let mut cpu_us: Vec<u128> = Vec::with_capacity(iters);
6424            for _ in 0..iters {
6425                let t0 = std::time::Instant::now();
6426                let out = cpu_build_parallel();
6427                cpu_us.push(t0.elapsed().as_micros());
6428                assert_eq!(out.len(), p_total * p_total);
6429            }
6430            cpu_us.sort_unstable();
6431            let cpu_median = cpu_us[iters / 2];
6432
6433            let speedup = (cpu_median as f64) / (gpu_median.max(1) as f64);
6434            eprintln!(
6435                "[bms_flex_row dense_block hill-climb] large-scale n={n} r={r} p={p_total}: \
6436                 cpu_median={cpu_median}us gpu_median={gpu_median}us \
6437                 speedup={speedup:.2}× (perf record; the gate is the policy decision)"
6438            );
6439            assert_row_batch_dispatch_worthy(
6440                "large-scale dense-H dispatch-worthiness gate",
6441                runtime.policy(),
6442                n,
6443            );
6444        }
6445    }
6446}