gam_solve/gpu_kernels/sae_resident.rs
1//! Device-resident SAE inner-iteration workspace for issue #1017.
2//!
3//! This first vertical slice keeps production fitting untouched. It accepts
4//! host-evaluated SAE basis/gate values plus already-assembled data-fit
5//! Arrow-Schur slabs, uploads those buffers once, and runs one Newton step
6//! through the existing GPU Arrow-Schur sequence when the runtime probe admits
7//! the workload. Later slices can replace the host slab feed with on-device
8//! basis/gate evaluation without changing the public step API.
9
10use ndarray::Array1;
11
12use crate::gpu_kernels::arrow_schur::{
13 ArrowSchurGpuFailure, ArrowSchurGpuSolution, solve_arrow_newton_step,
14 solve_arrow_newton_step_dense_reference,
15};
16use gam_problem::ExecutionPath;
17
18/// Per-iterate solve backend for the resident inner Newton loop.
19///
20/// All three modes run the IDENTICAL host control flow (`run_inner_loop`):
21/// residual-gradient assembly, LM trust-region accept/reject, ridge schedule.
22/// They differ ONLY in how the per-iterate arrow step is computed, which is
23/// exactly the residency lever #1017 measures:
24///
25/// * [`InnerSolveMode::DeviceResident`] — the Phase-3 fix: factor the constant
26/// Hessian blocks ONCE into a [`crate::gpu_kernels::arrow_schur::ResidentArrowFrameHandle`]
27/// and, every iterate, upload only the `O(n·d + k)` gradient and read back
28/// only `δ`. No per-solve D/B re-upload, no per-solve POTRF.
29/// * [`InnerSolveMode::DeviceReupload`] — the BEFORE path: call
30/// `solve_arrow_newton_step` per iterate, which re-packs and re-uploads
31/// `D`/`B`/`g` and re-runs the per-row POTRF + border Schur factor every call.
32/// This is the residency baseline the bench divides against.
33/// * [`InnerSolveMode::CpuReference`] — the dense f64 oracle (re-factors per
34/// iterate on the host), used for the correctness parity check.
35/// * [`InnerSolveMode::CpuArrow`] — the honest CPU COMPETITOR: the production
36/// structured Arrow-Schur solve (`ArrowSchurSystem::solve_with_options` with
37/// the device policy off), which exploits the block structure exactly as a
38/// CPU-only production host would. `CpuReference`'s dense
39/// `(n·d+k) × (n·d+k)` Cholesky is an ORACLE, not a competitor — quoting a
40/// speedup against it would flatter the device by a baseline no production
41/// host would ever run (#2393).
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum InnerSolveMode {
44 DeviceResident,
45 DeviceReupload,
46 CpuReference,
47 CpuArrow,
48}
49
50impl InnerSolveMode {
51 /// Truthful [`ExecutionPath`] this solve mode realizes (issue #1017): the
52 /// resident loop keeps factors on-device (`GpuResidentFull`), the baseline
53 /// re-uploads/re-factors every iterate (`GpuReupload`), and the reference
54 /// path runs on the host (`Cpu`).
55 #[inline]
56 const fn execution_path(self) -> ExecutionPath {
57 match self {
58 Self::DeviceResident => ExecutionPath::GpuResidentFull,
59 Self::DeviceReupload => ExecutionPath::GpuReupload,
60 Self::CpuReference | Self::CpuArrow => ExecutionPath::Cpu,
61 }
62 }
63
64 /// Whether this mode's per-iterate operator apply (`H·z`) may run on the
65 /// device.
66 ///
67 /// `CpuReference` is the INDEPENDENT host oracle the parity harness divides
68 /// against, so its contraction must stay on the host: routing it to the
69 /// device too would make "device == CPU" compare the device against itself,
70 /// and would make its wall clock a device measurement wearing a CPU label.
71 #[inline]
72 const fn operator_on_device(self) -> bool {
73 match self {
74 Self::DeviceResident | Self::DeviceReupload => true,
75 Self::CpuReference | Self::CpuArrow => false,
76 }
77 }
78}
79use crate::arrow_schur::{ArrowSchurError, ArrowSchurSystem};
80
81/// SAE shape used by the resident inner-iteration workspace.
82///
83/// `p` is the target width and current shared-border width for this slice. The
84/// true SAE decoder has richer `(basis × output)` structure; slice 1 deliberately
85/// keeps that structure host-assembled into `row_cross_slabs` while preserving
86/// the qwen-scale target width in the Schur border.
87#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88pub struct DeviceResidentArrowShape {
89 pub n: usize,
90 pub p: usize,
91 pub basis_cols: usize,
92 pub d: usize,
93}
94
95impl DeviceResidentArrowShape {
96 #[inline]
97 pub const fn qwen_non_gating() -> Self {
98 Self {
99 n: 2_000,
100 p: 2_048,
101 basis_cols: 8,
102 d: 2,
103 }
104 }
105
106 /// Color-arm shape from the #1017 measured gap (n=180, p=5120, M≈9, K=1):
107 /// few rows, very wide border. The dense-Schur device path (cuSOLVER border
108 /// POTRF) handles the `p=5120` border that exceeds the fused-kernel `P_MAX`.
109 #[inline]
110 pub const fn color_arm() -> Self {
111 Self {
112 n: 180,
113 p: 5_120,
114 basis_cols: 9,
115 d: 2,
116 }
117 }
118
119 #[inline]
120 pub const fn target_len(self) -> usize {
121 self.n * self.p
122 }
123
124 #[inline]
125 pub const fn basis_len(self) -> usize {
126 self.n * self.basis_cols
127 }
128
129 #[inline]
130 pub const fn row_hessian_len(self) -> usize {
131 self.n * self.d * self.d
132 }
133
134 #[inline]
135 pub const fn row_cross_len(self) -> usize {
136 self.n * self.d * self.p
137 }
138
139 #[inline]
140 pub const fn row_gradient_len(self) -> usize {
141 self.n * self.d
142 }
143
144 #[inline]
145 pub const fn border_hessian_len(self) -> usize {
146 self.p * self.p
147 }
148}
149
150/// Host-fed row-block slabs for the first resident slice.
151///
152/// All matrices are row-major in host memory:
153/// * `row_hessian_slabs`: `n` slabs of shape `d × d`.
154/// * `row_cross_slabs`: `n` slabs of shape `d × p`.
155/// * `border_hessian`: one `p × p` shared block.
156#[derive(Clone, Debug)]
157pub struct DeviceResidentArrowSlabs {
158 pub row_hessian_slabs: Vec<f64>,
159 pub row_cross_slabs: Vec<f64>,
160 pub row_gradient_slabs: Vec<f64>,
161 pub border_hessian: Vec<f64>,
162 pub border_gradient: Vec<f64>,
163}
164
165/// Result of one resident SAE inner Newton iteration.
166#[derive(Clone, Debug)]
167pub struct DeviceResidentArrowStep {
168 pub delta_t: Array1<f64>,
169 pub delta_beta: Array1<f64>,
170 pub objective: f64,
171 pub gradient_norm: f64,
172 pub log_det_hessian: f64,
173 pub execution_path: ExecutionPath,
174}
175
176#[derive(Debug, Clone)]
177pub enum DeviceResidentArrowError {
178 Shape { reason: String },
179 Unavailable { reason: String },
180 Solve { reason: String },
181}
182
183impl std::fmt::Display for DeviceResidentArrowError {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 match self {
186 Self::Shape { reason } | Self::Unavailable { reason } | Self::Solve { reason } => {
187 f.write_str(reason)
188 }
189 }
190 }
191}
192
193impl std::error::Error for DeviceResidentArrowError {}
194
195#[cfg(target_os = "linux")]
196pub struct DeviceResidentArrowBuffers {
197 pub stream: std::sync::Arc<cudarc::driver::CudaStream>,
198 pub target_x_dev: cudarc::driver::CudaSlice<f64>,
199 pub basis_values_dev: cudarc::driver::CudaSlice<f64>,
200 pub gate_activations_dev: cudarc::driver::CudaSlice<f64>,
201 pub row_hessian_dev: cudarc::driver::CudaSlice<f64>,
202 pub row_cross_dev: cudarc::driver::CudaSlice<f64>,
203 pub row_gradient_dev: cudarc::driver::CudaSlice<f64>,
204 pub border_hessian_dev: cudarc::driver::CudaSlice<f64>,
205 pub border_gradient_dev: cudarc::driver::CudaSlice<f64>,
206 pub bytes: usize,
207 /// cuBLAS handle bound to `stream`, created ONCE with the resident buffers.
208 /// The per-iterate operator applies below reuse it; creating a handle per
209 /// apply would reintroduce exactly the per-iteration device setup cost that
210 /// residency exists to remove.
211 blas: cudarc::cublas::CudaBlas,
212 /// Persistent device scratch for the per-iterate operator apply. Held
213 /// behind a mutex because the apply runs through `&self` (the multiplexed
214 /// driver shares `&workspace` across a rayon scope); each workspace owns its
215 /// own stream, handle, and scratch, so the lock is uncontended within a fit
216 /// and prevents two applies on ONE workspace from aliasing the scratch.
217 operator_scratch: std::sync::Mutex<DeviceOperatorScratch>,
218}
219
220/// Device-side operand/result buffers for one resident operator apply.
221///
222/// These are the only allocations the per-iterate `H·z` contraction needs, and
223/// they are `O(n·d + p)` — the `O(n·d·p)` cross slab and the `O(p²)` border stay
224/// resident in [`DeviceResidentArrowBuffers`] and are never re-uploaded.
225#[cfg(target_os = "linux")]
226struct DeviceOperatorScratch {
227 t_dev: cudarc::driver::CudaSlice<f64>,
228 beta_dev: cudarc::driver::CudaSlice<f64>,
229 cross_t_dev: cudarc::driver::CudaSlice<f64>,
230 cross_beta_dev: cudarc::driver::CudaSlice<f64>,
231 border_beta_dev: cudarc::driver::CudaSlice<f64>,
232}
233
234/// One application of the resident bordered-quadratic operator at an iterate
235/// `z = (t, β)`.
236///
237/// These three products are the ONLY `O(n·d·p)` / `O(p²)` work the inner Newton
238/// loop needs at a point, and BOTH the residual gradient and the objective are
239/// algebraic combinations of them:
240///
241/// ```text
242/// cross_t[i·d+r] = Σ_j H_tβ^(i)[r,j] · β_j (the (n·d)×p slab · β)
243/// cross_beta[j] = Σ_i Σ_r H_tβ^(i)[r,j] · t_{i,r} (that slab transposed · t)
244/// border_beta[j] = Σ_c H_ββ[j,c] · β_c (the p×p border · β)
245/// ```
246///
247/// so
248///
249/// ```text
250/// r(z)_t[i,r] = (H_tt^(i) t_i)_r + cross_t[i·d+r] − g₀_t[i,r]
251/// r(z)_β[j] = border_beta[j] + cross_beta[j] − g₀_β[j]
252/// φ(z) = ½‖X‖² + ½(Σ_{i,r} t·((H_tt t) + 2·cross_t) + Σ_j β_j·border_beta[j])
253/// − g₀ᵀz
254/// ```
255///
256/// The `H_tt` contribution is deliberately excluded: the per-row `d×d` blocks
257/// total `n·d²` entries (64 KiB at the qwen shape vs 64 MiB for the cross slab),
258/// so contracting them costs nothing and keeping them host-side avoids a kernel
259/// launch whose latency would exceed its arithmetic.
260#[derive(Clone, Debug)]
261struct ArrowOperatorApply {
262 cross_t: Vec<f64>,
263 cross_beta: Vec<f64>,
264 border_beta: Vec<f64>,
265}
266
267/// Upload-once workspace for the SAE data-fit Arrow-Schur inner iteration.
268pub struct DeviceResidentArrowWorkspace {
269 shape: DeviceResidentArrowShape,
270 target_x: Vec<f64>,
271 basis_values: Vec<f64>,
272 gate_activations: Vec<f64>,
273 slabs: DeviceResidentArrowSlabs,
274 #[cfg(target_os = "linux")]
275 device: Option<DeviceResidentArrowBuffers>,
276}
277
278impl DeviceResidentArrowWorkspace {
279 pub fn new(
280 shape: DeviceResidentArrowShape,
281 target_x: Vec<f64>,
282 basis_values: Vec<f64>,
283 gate_activations: Vec<f64>,
284 slabs: DeviceResidentArrowSlabs,
285 ) -> Result<Self, DeviceResidentArrowError> {
286 validate_shape(shape, &target_x, &basis_values, &gate_activations, &slabs)?;
287 #[cfg(target_os = "linux")]
288 let device =
289 upload_resident_buffers(shape, &target_x, &basis_values, &gate_activations, &slabs);
290 Ok(Self {
291 shape,
292 target_x,
293 basis_values,
294 gate_activations,
295 slabs,
296 #[cfg(target_os = "linux")]
297 device,
298 })
299 }
300
301 #[inline]
302 pub const fn shape(&self) -> DeviceResidentArrowShape {
303 self.shape
304 }
305
306 #[must_use]
307 pub fn device_resident(&self) -> bool {
308 #[cfg(target_os = "linux")]
309 {
310 self.device.is_some()
311 }
312 #[cfg(not(target_os = "linux"))]
313 {
314 false
315 }
316 }
317
318 #[must_use]
319 pub fn resident_device_bytes(&self) -> usize {
320 #[cfg(target_os = "linux")]
321 {
322 self.device.as_ref().map_or(0, |device| device.bytes)
323 }
324 #[cfg(not(target_os = "linux"))]
325 {
326 0
327 }
328 }
329
330 /// Opaque device-context identifier for telemetry: `1` when the resident
331 /// device buffers are live on this workspace, `0` when no device was bound.
332 /// Distinguishes "a device executed this fit" from "silent CPU fallback"
333 /// without leaking the cudarc handle.
334 #[must_use]
335 fn context_id(&self) -> usize {
336 usize::from(self.device_resident())
337 }
338
339 /// Bytes the re-uploading / frame-build path moves host→device for a full
340 /// `D`/`B`/`g`/border refresh, used to attribute H2D traffic in telemetry.
341 #[must_use]
342 fn frame_upload_bytes(&self) -> usize {
343 [
344 self.slabs.row_hessian_slabs.len(),
345 self.slabs.row_cross_slabs.len(),
346 self.slabs.row_gradient_slabs.len(),
347 self.slabs.border_hessian.len(),
348 self.slabs.border_gradient.len(),
349 ]
350 .into_iter()
351 .sum::<usize>()
352 * std::mem::size_of::<f64>()
353 }
354
355 #[must_use]
356 pub fn host_shadow_bytes(&self) -> usize {
357 [
358 self.target_x.len(),
359 self.basis_values.len(),
360 self.gate_activations.len(),
361 self.slabs.row_hessian_slabs.len(),
362 self.slabs.row_cross_slabs.len(),
363 self.slabs.row_gradient_slabs.len(),
364 self.slabs.border_hessian.len(),
365 self.slabs.border_gradient.len(),
366 ]
367 .into_iter()
368 .sum::<usize>()
369 * std::mem::size_of::<f64>()
370 }
371
372 /// Run one device-side Newton sequence. No CPU fallback is attempted here:
373 /// callers that want a reference path must call [`Self::cpu_reference_step`].
374 pub fn one_inner_iteration(
375 &self,
376 ridge_t: f64,
377 ridge_beta: f64,
378 ) -> Result<DeviceResidentArrowStep, DeviceResidentArrowError> {
379 if !self.device_resident() {
380 return Err(DeviceResidentArrowError::Unavailable {
381 reason: "SAE resident inner iteration unavailable: CUDA runtime did not admit the qwen-scale row-block workload".to_string(),
382 });
383 }
384 let sys = self.to_arrow_system();
385 let frame = crate::gpu_kernels::arrow_schur::ResidentArrowFrameHandle::new(
386 &sys, ridge_t, ridge_beta, None,
387 )
388 .map_err(map_gpu_error)?;
389 let g_t: Vec<f64> = sys
390 .rows
391 .iter()
392 .flat_map(|row| row.gt.iter().copied())
393 .collect();
394 let g_beta: Vec<f64> = sys.gb.iter().copied().collect();
395 // This is a SINGLE resident solve at one frozen gate/basis frame — the
396 // frame's constant Hessian factors are held resident and only the
397 // gradient is uploaded — so the truthful classifier is
398 // `GpuResidentLinearization`, not `GpuResidentFull` (which denotes the
399 // full multi-step device-resident inner Newton loop in `device_fit`).
400 frame
401 .solve_gradient(&g_t, &g_beta)
402 .map(|solution| self.finish_step(solution, ExecutionPath::GpuResidentLinearization))
403 .map_err(map_gpu_error)
404 }
405
406 /// CPU reference for parity harnesses. This path is explicit and is never
407 /// called from [`Self::one_inner_iteration`].
408 pub fn cpu_reference_step(
409 &self,
410 ridge_t: f64,
411 ridge_beta: f64,
412 ) -> Result<DeviceResidentArrowStep, DeviceResidentArrowError> {
413 let sys = self.to_arrow_system();
414 solve_arrow_newton_step_dense_reference(&sys, ridge_t, ridge_beta)
415 .map(|solution| self.finish_step(solution, ExecutionPath::Cpu))
416 .map_err(|reason| DeviceResidentArrowError::Solve { reason })
417 }
418
419 pub fn to_arrow_system(&self) -> ArrowSchurSystem {
420 let shape = self.shape;
421 let mut sys = ArrowSchurSystem::new(shape.n, shape.d, shape.p);
422 for i in 0..shape.n {
423 let h_base = i * shape.d * shape.d;
424 let b_base = i * shape.d * shape.p;
425 let g_base = i * shape.d;
426 for r in 0..shape.d {
427 for c in 0..shape.d {
428 sys.rows[i].htt[[r, c]] =
429 self.slabs.row_hessian_slabs[h_base + r * shape.d + c];
430 }
431 sys.rows[i].gt[r] = self.slabs.row_gradient_slabs[g_base + r];
432 for c in 0..shape.p {
433 sys.rows[i].htbeta[[r, c]] =
434 self.slabs.row_cross_slabs[b_base + r * shape.p + c];
435 }
436 }
437 }
438 for r in 0..shape.p {
439 sys.gb[r] = self.slabs.border_gradient[r];
440 for c in 0..shape.p {
441 sys.hbb[[r, c]] = self.slabs.border_hessian[r * shape.p + c];
442 }
443 }
444 sys.refresh_row_hessian_fingerprint();
445 sys
446 }
447
448 fn finish_step(
449 &self,
450 solution: crate::gpu_kernels::arrow_schur::ArrowSchurGpuSolution,
451 execution_path: ExecutionPath,
452 ) -> DeviceResidentArrowStep {
453 DeviceResidentArrowStep {
454 delta_t: solution.delta_t,
455 delta_beta: solution.delta_beta,
456 objective: 0.5 * squared_norm(&self.target_x),
457 gradient_norm: self.gradient_norm(),
458 log_det_hessian: solution.log_det_hessian,
459 execution_path,
460 }
461 }
462
463 fn gradient_norm(&self) -> f64 {
464 let row = squared_norm(&self.slabs.row_gradient_slabs);
465 let border = squared_norm(&self.slabs.border_gradient);
466 (row + border).sqrt()
467 }
468
469 // ---------------------------------------------------------------------
470 // Phase 3: full device-resident inner Newton loop (#1017).
471 //
472 // The resident slabs define a fixed bordered-quadratic data-fit objective
473 // φ(z) = ½‖X‖² + ½ zᵀ H z − g₀ᵀ z, z = (t, β),
474 // where `H` is the arrow-structured Hessian (per-row `H_tt`/`H_tβ` blocks
475 // plus the shared `H_ββ` border) and `g₀` is the base gradient assembled
476 // once at upload. This is the quadratic the SAE joint inner Newton actually
477 // minimises at a frozen gate/basis evaluation; the production driver
478 // (`LatentInnerSolver::solve`) re-linearises per outer evaluation, so a
479 // single resident frame is one such inner solve.
480 //
481 // The loop mirrors the production LM trust-region accept/reject exactly:
482 // at iterate `z` it forms the residual gradient `r(z) = H z − g₀`, takes
483 // the LM-damped arrow step (device or dense-reference), evaluates the trial
484 // objective, and accepts on the actual-vs-predicted reduction ratio. The
485 // iterate `(t, β)` and the per-step scalars (objective, gradient norm, ρ)
486 // are the ONLY host-side state; the heavy `O(n d³ + p³)` factor/solve stays
487 // on the resident buffers via `solve_arrow_newton_step`. For an exact
488 // quadratic the loop converges in one accepted step, but it exercises the
489 // full assemble→solve→objective→accept machinery and the scalar-only
490 // readback contract the production loop relies on.
491 // ---------------------------------------------------------------------
492
493 /// Run the full device-resident inner Newton loop. Routes the per-iteration
494 /// arrow solve through the GPU path; returns `Unavailable` when CUDA did not
495 /// admit the resident workload (callers wanting a CPU path use
496 /// [`Self::cpu_reference_fit`]).
497 pub fn device_fit(
498 &self,
499 opts: &DeviceResidentInnerOptions,
500 ) -> Result<DeviceResidentInnerOutcome, DeviceResidentArrowError> {
501 if !self.device_resident() {
502 return Err(DeviceResidentArrowError::Unavailable {
503 reason: "SAE resident inner loop unavailable: CUDA runtime did not admit the qwen-scale row-block workload".to_string(),
504 });
505 }
506 self.run_inner_loop(opts, InnerSolveMode::DeviceResident)
507 }
508
509 /// The #1017 residency baseline: run the SAME inner Newton loop but compute
510 /// each per-iterate arrow step through `solve_arrow_newton_step`, which
511 /// re-packs/re-uploads `D`/`B`/`g` and re-runs the per-row POTRF + border
512 /// Schur factor on EVERY iterate. This is the "current re-uploading path";
513 /// the bench divides [`Self::device_fit`] (resident) against it to isolate
514 /// the across-iteration residency speedup on one device, holding the host
515 /// control flow and the GPU factor kernels fixed.
516 pub fn device_reupload_fit(
517 &self,
518 opts: &DeviceResidentInnerOptions,
519 ) -> Result<DeviceResidentInnerOutcome, DeviceResidentArrowError> {
520 if !self.device_resident() {
521 return Err(DeviceResidentArrowError::Unavailable {
522 reason: "SAE re-uploading inner loop unavailable: CUDA runtime did not admit the row-block workload".to_string(),
523 });
524 }
525 self.run_inner_loop(opts, InnerSolveMode::DeviceReupload)
526 }
527
528 /// CPU dense-reference inner loop. Bit-for-bit the same host arithmetic as
529 /// [`Self::device_fit`] except the per-iteration arrow solve uses the dense
530 /// reference factorisation; the parity harness asserts the two agree.
531 ///
532 /// This is a correctness ORACLE, not a speed baseline: it factors the full
533 /// dense `(n·d+k) × (n·d+k)` joint Hessian, which is what makes it an
534 /// independent check of the arrow algebra, and also what makes its wall
535 /// clock meaningless as a CPU competitor. Use [`Self::cpu_arrow_fit`] for
536 /// the timing baseline.
537 pub fn cpu_reference_fit(
538 &self,
539 opts: &DeviceResidentInnerOptions,
540 ) -> Result<DeviceResidentInnerOutcome, DeviceResidentArrowError> {
541 self.run_inner_loop(opts, InnerSolveMode::CpuReference)
542 }
543
544 /// The honest CPU competitor: the SAME inner Newton loop with the
545 /// per-iterate step taken by the PRODUCTION structured Arrow-Schur solve on
546 /// the host (device policy off). This is what a CPU-only production host
547 /// runs, so it — not the dense oracle — is what a device speedup must be
548 /// divided against.
549 ///
550 /// `log_det_hessian` is reported as NaN for this mode: the production solve
551 /// entry returns the step and its PCG diagnostics, not the joint log
552 /// determinant, and fabricating one from a second factorisation would put
553 /// work in the baseline that the production path does not do.
554 pub fn cpu_arrow_fit(
555 &self,
556 opts: &DeviceResidentInnerOptions,
557 ) -> Result<DeviceResidentInnerOutcome, DeviceResidentArrowError> {
558 self.run_inner_loop(opts, InnerSolveMode::CpuArrow)
559 }
560
561 fn run_inner_loop(
562 &self,
563 opts: &DeviceResidentInnerOptions,
564 mode: InnerSolveMode,
565 ) -> Result<DeviceResidentInnerOutcome, DeviceResidentArrowError> {
566 let execution_path = mode.execution_path();
567 let n = self.shape.n;
568 let d = self.shape.d;
569 let p = self.shape.p;
570 let t_len = n * d;
571
572 // Resident iterate, host-side scalars only. The device buffers (X,
573 // slabs, border) never leave the device across iterations; only this
574 // O(t_len + p) iterate and the per-step reduction scalars cross back.
575 let mut t = vec![0.0_f64; t_len];
576 let mut beta = vec![0.0_f64; p];
577
578 let base = self.to_arrow_system();
579 let half_target_energy = 0.5 * squared_norm(&self.target_x);
580 // ONE residual system for the whole fit: the Hessian blocks are frozen
581 // for this frame, so only its gradients move (see `residual_into`).
582 let mut residual = self.to_arrow_system();
583 let mut accounting = ResidencyAccounting::default();
584
585 let mut ridge_t = opts.initial_ridge_t.max(0.0);
586 let mut ridge_beta = opts.initial_ridge_beta.max(0.0);
587 // #1017 Phase 3: when running on device, keep the resident Arrow frame
588 // (constant Hessian blocks + their factors) on the device across
589 // iterations. The frame bakes a fixed `(ridge_t, ridge_beta)` into the
590 // per-row and border Cholesky factors, so it is rebuilt only when the LM
591 // ridge changes (reject/shrink); every iteration that shares the cached
592 // ridge reuses the resident factors and uploads only the `O(n·d + p)`
593 // gradient. The CPU reference path keeps re-factoring per iterate so the
594 // parity harness compares residency against a fully independent solve.
595 let mut frames = SharedFrameState::default();
596 let mut current_apply = accounting.apply(self, mode, &t, &beta);
597 let mut current_objective =
598 self.objective_from_apply(&base, half_target_energy, ¤t_apply, &t, &beta);
599 let mut accepted_iters = 0_usize;
600 let mut total_iters = 0_usize;
601 let mut converged = false;
602 let mut last_step = DeviceResidentArrowStep {
603 delta_t: Array1::zeros(t_len),
604 delta_beta: Array1::zeros(p),
605 objective: current_objective,
606 gradient_norm: 0.0,
607 log_det_hessian: 0.0,
608 execution_path,
609 };
610
611 while total_iters < opts.max_iterations {
612 // Residual gradient r(z) = H z − g₀ becomes the system gradient,
613 // read off the operator apply already computed at this iterate.
614 self.residual_into(&mut residual, &base, ¤t_apply, &t);
615 let g_norm = arrow_system_gradient_norm(&residual);
616 let scale = 1.0 + iterate_norm(&t, &beta);
617 if g_norm / scale < opts.convergence_tolerance {
618 converged = true;
619 break;
620 }
621
622 let solve_start = std::time::Instant::now();
623 let solution = match mode {
624 InnerSolveMode::DeviceResident => {
625 // #2539: one two-level frame for the whole loop. An unchanged
626 // ridge reuses the ridge-keyed factors and uploads only the
627 // `O(n·d + p)` gradient; a ridge change re-factors from the
628 // resident base blocks instead of re-packing and re-uploading
629 // the slabs. Either failure becomes a Solve error so the
630 // LM-escalation arm below grows the ridge and retries,
631 // identical to a per-iterate solve failure.
632 self.resident_step(
633 &mut frames,
634 &mut accounting,
635 &residual,
636 ridge_t,
637 ridge_beta,
638 )
639 }
640 InnerSolveMode::DeviceReupload => {
641 // #1017 residency baseline: re-upload D/B/g and re-factor on
642 // every iterate. Same GPU factor kernels as the resident path,
643 // minus the across-iteration buffer/factor reuse — so EVERY
644 // iterate creates handles, factorizes, launches, and re-uploads
645 // the full slabs.
646 gam_gpu::profile::telemetry_record_handle_creation(self.context_id());
647 gam_gpu::profile::telemetry_record_factorization();
648 gam_gpu::profile::telemetry_record_h2d(self.frame_upload_bytes());
649 gam_gpu::profile::telemetry_record_kernel_launch();
650 gam_gpu::profile::telemetry_record_d2h(
651 (n * d + p) * std::mem::size_of::<f64>(),
652 );
653 solve_arrow_newton_step(&residual, ridge_t, ridge_beta, None)
654 .map_err(map_gpu_error)
655 }
656 InnerSolveMode::CpuReference => {
657 solve_arrow_newton_step_dense_reference(&residual, ridge_t, ridge_beta)
658 .map_err(|reason| DeviceResidentArrowError::Solve { reason })
659 }
660 InnerSolveMode::CpuArrow => {
661 cpu_arrow_newton_step(&residual, ridge_t, ridge_beta)
662 }
663 };
664
665 accounting.solve_seconds += solve_start.elapsed().as_secs_f64();
666 let solution = match solution {
667 Ok(sol) => sol,
668 Err(DeviceResidentArrowError::Solve { .. })
669 | Err(DeviceResidentArrowError::Unavailable { .. }) => {
670 // LM escalation: grow ridge, retry without consuming an
671 // iteration. Mirrors the production per-row/Schur PD-failure
672 // arm in `LatentInnerSolver::solve`.
673 ridge_t = grow_ridge(ridge_t, opts.lm_grow);
674 ridge_beta = grow_ridge(ridge_beta, opts.lm_grow);
675 if ridge_t > opts.max_ridge || ridge_beta > opts.max_ridge {
676 return Err(DeviceResidentArrowError::Solve {
677 reason: format!(
678 "SAE resident inner loop: LM ridge exceeded max ({:e}) at iter {total_iters}",
679 opts.max_ridge
680 ),
681 });
682 }
683 total_iters += 1;
684 continue;
685 }
686 Err(other) => return Err(other),
687 };
688
689 // Predicted reduction from the bare quadratic model on the residual
690 // system, identical formula to the production trust-region ratio.
691 let predicted_reduction = crate::arrow_schur::arrow_bare_quadratic_model_reduction(
692 &residual,
693 solution.delta_t.view(),
694 solution.delta_beta.view(),
695 ridge_t,
696 ridge_beta,
697 )
698 .map_err(|err| DeviceResidentArrowError::Solve {
699 reason: format!("SAE resident inner loop predicted-reduction failed: {err}"),
700 })?;
701
702 // Trial iterate.
703 let mut trial_t = t.clone();
704 let mut trial_beta = beta.clone();
705 for (slot, dv) in trial_t.iter_mut().zip(solution.delta_t.iter()) {
706 *slot += *dv;
707 }
708 for (slot, dv) in trial_beta.iter_mut().zip(solution.delta_beta.iter()) {
709 *slot += *dv;
710 }
711 let trial_apply = accounting.apply(self, mode, &trial_t, &trial_beta);
712 let trial_objective = self.objective_from_apply(
713 &base,
714 half_target_energy,
715 &trial_apply,
716 &trial_t,
717 &trial_beta,
718 );
719
720 // Trust-region gain-ratio noise floor keyed to the objective's own
721 // magnitude, mirroring the production `LatentInnerSolver` (#1127): the
722 // floor must be equivariant under a response rescaling `y → a·y` (the
723 // penalized objective and both reductions scale as `O(a²)`). The
724 // previous `.max(1.0)` absolute floor broke this — near a converged
725 // iterate it pinned the floor at `1e-14` while a genuine refining
726 // step's `predicted_reduction` was `O(a²)`, misclassifying the real
727 // step as numerical noise and stalling the inner solve at a
728 // non-stationary point. A perfectly converged objective
729 // (`current_objective == 0`) yields a `0` floor, so the
730 // `predicted_reduction > 0` branch still governs and no step is lost.
731 let objective_scale = current_objective.abs();
732 let noise_floor = objective_scale * 1e-14;
733 let actual_reduction = current_objective - trial_objective;
734 let rho = if predicted_reduction > noise_floor {
735 actual_reduction / predicted_reduction
736 } else if actual_reduction >= -noise_floor {
737 1.0
738 } else {
739 -1.0
740 };
741
742 if rho > 0.0 && trial_objective.is_finite() {
743 t = trial_t;
744 beta = trial_beta;
745 current_apply = trial_apply;
746 current_objective = trial_objective;
747 ridge_t = (ridge_t * opts.lm_shrink).max(0.0);
748 ridge_beta = (ridge_beta * opts.lm_shrink).max(0.0);
749 last_step = DeviceResidentArrowStep {
750 delta_t: solution.delta_t,
751 delta_beta: solution.delta_beta,
752 objective: current_objective,
753 gradient_norm: g_norm,
754 log_det_hessian: solution.log_det_hessian,
755 execution_path,
756 };
757 accepted_iters += 1;
758 total_iters += 1;
759 } else {
760 ridge_t = grow_ridge(ridge_t, opts.lm_grow);
761 ridge_beta = grow_ridge(ridge_beta, opts.lm_grow);
762 if ridge_t > opts.max_ridge || ridge_beta > opts.max_ridge {
763 return Err(DeviceResidentArrowError::Solve {
764 reason: format!(
765 "SAE resident inner loop: LM rejected step until ridge exceeded max ({:e}) at iter {total_iters} (rho={rho:.3e})",
766 opts.max_ridge
767 ),
768 });
769 }
770 total_iters += 1;
771 }
772 }
773
774 Ok(DeviceResidentInnerOutcome {
775 t: Array1::from_vec(t),
776 beta: Array1::from_vec(beta),
777 objective: current_objective,
778 gradient_norm: last_step.gradient_norm,
779 log_det_hessian: last_step.log_det_hessian,
780 iterations: total_iters,
781 accepted_iterations: accepted_iters,
782 converged,
783 execution_path,
784 residency: accounting.finish(),
785 })
786 }
787
788 // ---------------------------------------------------------------------
789 // Phase 3b: reuse the resident frame ACROSS OUTER iterations (#1017
790 // deliverable 3).
791 //
792 // The inner Newton loop above already keeps the resident Arrow frame
793 // (factored `D`/`B`/Schur) on the device across INNER iterations at a fixed
794 // ridge. The next residency tier is the OUTER loop: across consecutive outer
795 // evaluations the SAE Hessian operator is unchanged whenever the frozen
796 // gate/basis frame (hence `D = H_tt`, `B = H_tβ`, border `H_ββ`) does not
797 // move — only the base gradient `g₀` (the linearization point / target
798 // residual) changes. In that regime the `O(n·d³ + p³)` factor work and the
799 // dominant `O(n·d·p)` `D`/`B` upload need to happen ONCE for the whole outer
800 // sweep, not once per outer. `device_fit_outer_sequence` realizes that: it
801 // builds at most ONE resident frame for an unchanged operator and drives
802 // every outer's inner solve through it, re-uploading only the per-outer
803 // `O(n·d + p)` gradient. The per-outer parity oracle is an independent
804 // `device_fit` (fresh frame per outer); the two must agree because sharing
805 // the factor across outers skips only re-deriving operator-independent work.
806 // ---------------------------------------------------------------------
807
808 /// Apply the resident bordered-quadratic operator at `z = (t, β)`.
809 ///
810 /// This is the ONE `O(n·d·p + p²)` evaluation per visited point; the
811 /// residual gradient AND the objective are both read off the returned
812 /// products (see [`ArrowOperatorApply`]), so the loop below evaluates it
813 /// once per trial iterate instead of walking the cross slab three times.
814 ///
815 /// Routing is magic-by-default and shape-driven: the workspace is
816 /// device-resident exactly when `route_through_gpu` admitted the slab
817 /// upload at construction, so a below-break-even shape simply never has
818 /// device buffers and takes the host arm. A device apply that faults falls
819 /// back to the host arm and is reported once per process — never a silent
820 /// downgrade.
821 fn apply_operator(
822 &self,
823 on_device: bool,
824 t: &[f64],
825 beta: &[f64],
826 ) -> (ArrowOperatorApply, OperatorApplyCost) {
827 #[cfg(target_os = "linux")]
828 {
829 if on_device && self.device.is_some() {
830 match self.apply_operator_device(t, beta) {
831 Some(apply) => {
832 let moved = (t.len() + beta.len()) * std::mem::size_of::<f64>();
833 let returned = (t.len() + 2 * beta.len()) * std::mem::size_of::<f64>();
834 gam_gpu::profile::telemetry_record_h2d(moved);
835 gam_gpu::profile::telemetry_record_kernel_launch();
836 gam_gpu::profile::telemetry_record_d2h(returned);
837 return (
838 apply,
839 OperatorApplyCost {
840 on_device: true,
841 host_to_device_bytes: moved,
842 device_to_host_bytes: returned,
843 },
844 );
845 }
846 None => gam_gpu::engagement::note_route_engagement(
847 "gam-solve sae_resident inner step",
848 "CPU reference",
849 false,
850 "resident operator apply faulted on device; host contraction fallback",
851 ),
852 }
853 }
854 }
855 // Reaching here with `on_device` set means the device arm was asked for
856 // and not taken: the workspace holds no device buffers (below the
857 // upload break-even, or a runtime that never admitted the slab), the
858 // apply faulted, or this target has no CUDA at all. All three are the
859 // downgrade the doc comment above promises is never silent, so report
860 // it through the same once-per-process channel the fault path uses.
861 // This also consumes `on_device` on every target, which is why the
862 // parameter does not need — and must not have — an underscore.
863 if on_device {
864 gam_gpu::engagement::note_route_engagement(
865 "gam-solve sae_resident inner step",
866 "CPU reference",
867 false,
868 "resident operator apply requested the device arm but the workspace is not \
869 device-resident; host contraction",
870 );
871 }
872 (
873 self.apply_operator_host(t, beta),
874 OperatorApplyCost::default(),
875 )
876 }
877
878 /// Host arm of [`Self::apply_operator`]: two sequential sweeps over the
879 /// flat row-major cross slab plus one over the border.
880 ///
881 /// Reading the slabs FLAT is the point. The `ArrowSchurSystem` view of the
882 /// same data indexes `rows[i].htbeta[[r, j]]`, so accumulating
883 /// `Σ_i Σ_r H_tβ^(i)[r,j]·t` with `j` outermost walks `n` separate `d×p`
884 /// allocations with stride `p` — `n·d·p` cache-missing loads. The flat form
885 /// touches every byte exactly once in address order.
886 ///
887 /// Parallelism is deterministic by construction: the per-row `cross_t` and
888 /// per-border-row `border_beta` entries are independent, and the one real
889 /// reduction (`cross_beta`) is folded from fixed-size row chunks in chunk
890 /// order, so the result does not depend on the thread count or scheduling.
891 fn apply_operator_host(&self, t: &[f64], beta: &[f64]) -> ArrowOperatorApply {
892 let n = self.shape.n;
893 let d = self.shape.d;
894 let p = self.shape.p;
895 let cross = self.slabs.row_cross_slabs.as_slice();
896 let border = self.slabs.border_hessian.as_slice();
897
898 // Chunk width depends only on `n`, so the reduction order is identical
899 // on every host and at every thread count.
900 let chunk_rows = n.div_ceil(OPERATOR_MAX_ROW_CHUNKS).max(OPERATOR_MIN_ROW_CHUNK);
901 let parallel = n >= OPERATOR_PARALLEL_ROW_MIN && rayon::current_thread_index().is_none();
902
903 let mut cross_t = vec![0.0_f64; n * d];
904 let mut cross_beta = vec![0.0_f64; p];
905 let row_chunk = |rows: std::ops::Range<usize>,
906 cross_t_chunk: &mut [f64],
907 cross_beta_partial: &mut [f64]| {
908 for (local, i) in rows.enumerate() {
909 for r in 0..d {
910 let base = (i * d + r) * p;
911 let slab = &cross[base..base + p];
912 let mut acc = 0.0_f64;
913 for (j, &value) in slab.iter().enumerate() {
914 acc += value * beta[j];
915 }
916 cross_t_chunk[local * d + r] = acc;
917 let weight = t[i * d + r];
918 for (j, &value) in slab.iter().enumerate() {
919 cross_beta_partial[j] += value * weight;
920 }
921 }
922 }
923 };
924
925 if parallel {
926 use rayon::prelude::*;
927 let partials: Vec<Vec<f64>> = cross_t
928 .par_chunks_mut(chunk_rows * d)
929 .enumerate()
930 .map(|(chunk_idx, cross_t_chunk)| {
931 let start = chunk_idx * chunk_rows;
932 let end = (start + chunk_rows).min(n);
933 let mut partial = vec![0.0_f64; p];
934 row_chunk(start..end, cross_t_chunk, &mut partial);
935 partial
936 })
937 .collect();
938 // Fold in chunk order: bit-identical to the sequential arm's chunk
939 // sequence regardless of how the chunks were scheduled.
940 for partial in &partials {
941 for (slot, &value) in cross_beta.iter_mut().zip(partial.iter()) {
942 *slot += value;
943 }
944 }
945 } else {
946 let mut chunk_start = 0_usize;
947 while chunk_start < n {
948 let chunk_end = (chunk_start + chunk_rows).min(n);
949 let mut partial = vec![0.0_f64; p];
950 let slice = &mut cross_t[chunk_start * d..chunk_end * d];
951 row_chunk(chunk_start..chunk_end, slice, &mut partial);
952 for (slot, value) in cross_beta.iter_mut().zip(partial) {
953 *slot += value;
954 }
955 chunk_start = chunk_end;
956 }
957 }
958
959 let border_row = |r: usize| -> f64 {
960 let row = &border[r * p..(r + 1) * p];
961 let mut acc = 0.0_f64;
962 for (c, &value) in row.iter().enumerate() {
963 acc += value * beta[c];
964 }
965 acc
966 };
967 let border_beta: Vec<f64> = if parallel {
968 use rayon::prelude::*;
969 (0..p).into_par_iter().map(border_row).collect()
970 } else {
971 (0..p).map(border_row).collect()
972 };
973
974 ArrowOperatorApply {
975 cross_t,
976 cross_beta,
977 border_beta,
978 }
979 }
980
981 /// Device arm of [`Self::apply_operator`]: three cuBLAS `dgemv` launches
982 /// against the ALREADY-RESIDENT cross slab and border, moving only the
983 /// `O(n·d + p)` iterate up and the `O(n·d + p)` products back. The
984 /// `O(n·d·p)` and `O(p²)` operands never cross the bus after upload.
985 #[cfg(target_os = "linux")]
986 fn apply_operator_device(&self, t: &[f64], beta: &[f64]) -> Option<ArrowOperatorApply> {
987 use cudarc::cublas::sys::cublasOperation_t;
988 use cudarc::cublas::{Gemv, GemvConfig};
989
990 let device = self.device.as_ref()?;
991 let rows = self.shape.n.checked_mul(self.shape.d)?;
992 let p = self.shape.p;
993 let rows_i = i32::try_from(rows).ok()?;
994 let p_i = i32::try_from(p).ok()?;
995 let mut guard = device.operator_scratch.lock().ok()?;
996 let scratch = &mut *guard;
997 device.stream.memcpy_htod(t, &mut scratch.t_dev).ok()?;
998 device
999 .stream
1000 .memcpy_htod(beta, &mut scratch.beta_dev)
1001 .ok()?;
1002
1003 // `row_cross_dev` holds the `(n·d)×p` cross slab ROW-major, which cuBLAS
1004 // reads as the `p×(n·d)` COLUMN-major matrix `Aᵀ` with `lda = p`. So
1005 // `A·β` is the transposed product and `Aᵀ·t` the untransposed one — no
1006 // repacking, no transpose copy.
1007 let cross_t_cfg = GemvConfig::<f64> {
1008 trans: cublasOperation_t::CUBLAS_OP_T,
1009 m: p_i,
1010 n: rows_i,
1011 alpha: 1.0,
1012 lda: p_i,
1013 incx: 1,
1014 beta: 0.0,
1015 incy: 1,
1016 };
1017 // SAFETY: `row_cross_dev` is the live `p×(n·d)` column-major slab with
1018 // `lda = p` uploaded at construction and validated against the shape;
1019 // `beta_dev` holds `p` entries (the `m` operand length under OP_T) and
1020 // `cross_t_dev` holds `n·d` (the `n` result length), both unit-stride.
1021 unsafe {
1022 device.blas.gemv(
1023 cross_t_cfg,
1024 &device.row_cross_dev,
1025 &scratch.beta_dev,
1026 &mut scratch.cross_t_dev,
1027 )
1028 }
1029 .ok()?;
1030
1031 let cross_beta_cfg = GemvConfig::<f64> {
1032 trans: cublasOperation_t::CUBLAS_OP_N,
1033 m: p_i,
1034 n: rows_i,
1035 alpha: 1.0,
1036 lda: p_i,
1037 incx: 1,
1038 beta: 0.0,
1039 incy: 1,
1040 };
1041 // SAFETY: same slab as above; under OP_N the operand length is `n·d`
1042 // (`t_dev`) and the result length is `p` (`cross_beta_dev`), unit-stride.
1043 unsafe {
1044 device.blas.gemv(
1045 cross_beta_cfg,
1046 &device.row_cross_dev,
1047 &scratch.t_dev,
1048 &mut scratch.cross_beta_dev,
1049 )
1050 }
1051 .ok()?;
1052
1053 // `border_hessian_dev` is the `p×p` border ROW-major, i.e. `H_ββᵀ`
1054 // column-major with `lda = p`; the transposed GEMV recovers `H_ββ·β`.
1055 let border_cfg = GemvConfig::<f64> {
1056 trans: cublasOperation_t::CUBLAS_OP_T,
1057 m: p_i,
1058 n: p_i,
1059 alpha: 1.0,
1060 lda: p_i,
1061 incx: 1,
1062 beta: 0.0,
1063 incy: 1,
1064 };
1065 // SAFETY: `border_hessian_dev` is the live `p×p` slab with `lda = p`;
1066 // `beta_dev` and `border_beta_dev` each hold `p` unit-stride entries.
1067 unsafe {
1068 device.blas.gemv(
1069 border_cfg,
1070 &device.border_hessian_dev,
1071 &scratch.beta_dev,
1072 &mut scratch.border_beta_dev,
1073 )
1074 }
1075 .ok()?;
1076
1077 let cross_t = device.stream.clone_dtoh(&scratch.cross_t_dev).ok()?;
1078 let cross_beta = device.stream.clone_dtoh(&scratch.cross_beta_dev).ok()?;
1079 let border_beta = device.stream.clone_dtoh(&scratch.border_beta_dev).ok()?;
1080 Some(ArrowOperatorApply {
1081 cross_t,
1082 cross_beta,
1083 border_beta,
1084 })
1085 }
1086
1087 /// Bordered-quadratic objective `½‖X‖² + ½ zᵀ H z − g₀ᵀ z` at the iterate
1088 /// whose operator apply is `apply`. Only the `O(n·d²)` per-row `H_tt`
1089 /// contraction and `O(n·d + p)` dots remain here — the heavy products were
1090 /// already formed (on device when admitted) by [`Self::apply_operator`].
1091 fn objective_from_apply(
1092 &self,
1093 base: &ArrowSchurSystem,
1094 half_target_energy: f64,
1095 apply: &ArrowOperatorApply,
1096 t: &[f64],
1097 beta: &[f64],
1098 ) -> f64 {
1099 let n = self.shape.n;
1100 let d = self.shape.d;
1101 let p = self.shape.p;
1102 // quad = zᵀ H z, lin = g₀ᵀ z.
1103 let mut quad = 0.0_f64;
1104 let mut lin = 0.0_f64;
1105 for i in 0..n {
1106 let t_base = i * d;
1107 for r in 0..d {
1108 let mut htt_t = 0.0_f64;
1109 for c in 0..d {
1110 htt_t += base.rows[i].htt[[r, c]] * t[t_base + c];
1111 }
1112 quad += t[t_base + r] * (htt_t + 2.0 * apply.cross_t[t_base + r]);
1113 lin += base.rows[i].gt[r] * t[t_base + r];
1114 }
1115 }
1116 for r in 0..p {
1117 quad += beta[r] * apply.border_beta[r];
1118 lin += base.gb[r] * beta[r];
1119 }
1120 half_target_energy + 0.5 * quad - lin
1121 }
1122
1123 /// Overwrite `sys`'s gradients with the residual `r(z) = H z − g₀` at the
1124 /// iterate whose operator apply is `apply`, leaving every Hessian block (and
1125 /// the row-Hessian fingerprint, which those blocks alone determine) intact.
1126 ///
1127 /// The system is built ONCE per fit and mutated in place from here on. The
1128 /// old per-iteration `to_arrow_system()` rebuild re-materialised all `n`
1129 /// `d×p` cross blocks and the `p×p` border — 96 MiB of allocation and copy
1130 /// at the qwen shape — and then re-hashed them through
1131 /// `refresh_row_hessian_fingerprint` (documented as "intentionally
1132 /// expensive"), every single iterate, to reproduce values that cannot move
1133 /// while the frame is frozen.
1134 fn residual_into(
1135 &self,
1136 sys: &mut ArrowSchurSystem,
1137 base: &ArrowSchurSystem,
1138 apply: &ArrowOperatorApply,
1139 t: &[f64],
1140 ) {
1141 let n = self.shape.n;
1142 let d = self.shape.d;
1143 let p = self.shape.p;
1144 for i in 0..n {
1145 let t_base = i * d;
1146 for r in 0..d {
1147 let mut hz = 0.0_f64;
1148 for c in 0..d {
1149 hz += base.rows[i].htt[[r, c]] * t[t_base + c];
1150 }
1151 hz += apply.cross_t[t_base + r];
1152 sys.rows[i].gt[r] = hz - base.rows[i].gt[r];
1153 }
1154 }
1155 for r in 0..p {
1156 sys.gb[r] = apply.border_beta[r] + apply.cross_beta[r] - base.gb[r];
1157 }
1158 }
1159
1160 /// One per-iterate device-resident Newton solve through the two-level frame
1161 /// (#2539).
1162 ///
1163 /// `residual` carries the CONSTANT Hessian blocks and the iterate's gradient:
1164 /// `residual_into` above writes `gt`/`gb` only, so `H_tt`/`H_tβ`/`H_ββ` are
1165 /// fixed for the whole loop and nothing about them needs to cross the bus more
1166 /// than once. Routing:
1167 ///
1168 /// * ridge unchanged since the last solve — reuse the ridge-keyed FACTORS
1169 /// (`L_i`, `Y_i`, `L_S`) sitting on top of the resident base blocks and
1170 /// upload only the `O(n·d + p)` gradient. No POTRF runs. This is the
1171 /// whole no-rejection trajectory and it must stay free: the two-level
1172 /// split exists precisely so adopting the base frame for ridge changes
1173 /// does not put a factor chain back on the accepted step.
1174 /// * ridge changed — re-factor from the already-resident base blocks. Only
1175 /// the two ridge scalars, the re-diagonalised `D` (`n·d·d`) and the
1176 /// gradient cross to the device, in place of a host re-pack plus a full
1177 /// re-upload of the `n·d×p` slabs.
1178 /// * base frame unavailable (no CUDA, or a matrix-free `H_ββ`/`H_tβ`
1179 /// operator the dense device path does not admit) — fall back to the
1180 /// pre-#2539 ridge-keyed rebuild, so a host that cannot hold the base
1181 /// blocks behaves exactly as it did.
1182 ///
1183 /// A numerical refusal (`RidgeBumpRequired` / `SchurFactorFailed`) is returned
1184 /// rather than retried on the other arm: the caller's LM arm grows the ridge
1185 /// and retries, which is what a failed frame build already did.
1186 ///
1187 /// `accounting` receives the FRAME-BUILD half of this call's cost separately
1188 /// from the gradient solve. The issue's headline number is a split — "~277 of
1189 /// 505 ms is frame build" — and no counter on the fit could express it, so the
1190 /// only way to check the residency claim was an external profiler on a card
1191 /// nobody could rerun. The three build regions timed here (base-block upload,
1192 /// on-device re-factor, legacy host-rebuild) are exactly the work residency is
1193 /// supposed to remove.
1194 fn resident_step(
1195 &self,
1196 frames: &mut SharedFrameState,
1197 accounting: &mut ResidencyAccounting,
1198 residual: &ArrowSchurSystem,
1199 ridge_t: f64,
1200 ridge_beta: f64,
1201 ) -> Result<ArrowSchurGpuSolution, DeviceResidentArrowError> {
1202 let n = self.shape.n;
1203 let d = self.shape.d;
1204 let p = self.shape.p;
1205 let mut g_t = Vec::with_capacity(n * d);
1206 for row in &residual.rows {
1207 for &v in row.gt.iter() {
1208 g_t.push(v);
1209 }
1210 }
1211 let g_beta: Vec<f64> = residual.gb.iter().copied().collect();
1212 let gradient_bytes = (g_t.len() + g_beta.len()) * std::mem::size_of::<f64>();
1213 let readback_bytes = (n * d + p) * std::mem::size_of::<f64>();
1214
1215 // Level 1: the resident base blocks. Built at most once per loop.
1216 if !frames.base_declined && frames.base.is_none() {
1217 let build_start = std::time::Instant::now();
1218 let built =
1219 crate::gpu_kernels::arrow_schur::ResidentBaseArrowFrameHandle::new(residual, None);
1220 accounting.frame_build_seconds += build_start.elapsed().as_secs_f64();
1221 match built {
1222 Ok(base) => {
1223 gam_gpu::profile::telemetry_record_handle_creation(self.context_id());
1224 gam_gpu::profile::telemetry_record_h2d(self.frame_upload_bytes());
1225 frames.base_builds += 1;
1226 accounting.base_builds += 1;
1227 frames.base = Some(base);
1228 }
1229 Err(err) => {
1230 frames.base_declined = true;
1231 gam_gpu::engagement::note_route_engagement(
1232 "gam-solve sae_resident inner step",
1233 "CPU reference",
1234 false,
1235 &format!(
1236 "SAE base-resident frame declined; ridge changes fall back to the \
1237 host rebuild: {}",
1238 map_gpu_error(err)
1239 ),
1240 );
1241 }
1242 }
1243 }
1244 if let Some(base) = frames.base.as_ref() {
1245 // Level 2: the ridge-keyed factors on top of those blocks. A hit is a
1246 // gradient-only solve — the accepted-step path, which must not pay a
1247 // POTRF.
1248 let factors_match = frames
1249 .base_keyed
1250 .as_ref()
1251 .is_some_and(|factors| factors.matches_ridge(ridge_t, ridge_beta));
1252 if !factors_match {
1253 // The ridge moved. Drop the stale factors before building the new
1254 // ones so a failed factor cannot leave a key naming device state
1255 // that no longer matches it.
1256 frames.base_keyed = None;
1257 let refactor_start = std::time::Instant::now();
1258 let factored = base.factor_at(ridge_t, ridge_beta);
1259 accounting.frame_build_seconds += refactor_start.elapsed().as_secs_f64();
1260 let factors = factored.map_err(map_gpu_error)?;
1261 frames.base_refactors += 1;
1262 accounting.base_refactors += 1;
1263 gam_gpu::profile::telemetry_record_factorization();
1264 gam_gpu::profile::telemetry_record_h2d(n * d * d * std::mem::size_of::<f64>());
1265 frames.base_keyed = Some(factors);
1266 } else {
1267 frames.base_gradient_solves += 1;
1268 accounting.base_gradient_solves += 1;
1269 }
1270 match frames.base_keyed.as_ref() {
1271 Some(factors) => {
1272 gam_gpu::profile::telemetry_record_h2d(gradient_bytes);
1273 gam_gpu::profile::telemetry_record_kernel_launch();
1274 gam_gpu::profile::telemetry_record_d2h(readback_bytes);
1275 return base
1276 .solve_with_factors(factors, &g_t, &g_beta)
1277 .map_err(map_gpu_error);
1278 }
1279 None => {
1280 return Err(DeviceResidentArrowError::Solve {
1281 reason: "SAE two-level frame lost its ridge-keyed factors after building \
1282 them"
1283 .to_string(),
1284 });
1285 }
1286 }
1287 }
1288
1289 // No base frame on this host: the pre-#2539 ridge-keyed rebuild, with its
1290 // own same-ridge reuse so a CPU-declined host keeps the behaviour it had.
1291 let keyed_matches = frames
1292 .keyed
1293 .as_ref()
1294 .is_some_and(|(rt, rb, _)| *rt == ridge_t && *rb == ridge_beta);
1295 if keyed_matches {
1296 match frames.keyed.as_ref() {
1297 Some((_, _, frame)) => {
1298 gam_gpu::profile::telemetry_record_h2d(gradient_bytes);
1299 gam_gpu::profile::telemetry_record_kernel_launch();
1300 gam_gpu::profile::telemetry_record_d2h(readback_bytes);
1301 return frame.solve_gradient(&g_t, &g_beta).map_err(map_gpu_error);
1302 }
1303 None => {
1304 return Err(DeviceResidentArrowError::Solve {
1305 reason: "SAE two-level frame lost its ridge-keyed frame after matching it"
1306 .to_string(),
1307 });
1308 }
1309 }
1310 }
1311 frames.keyed = None;
1312 let rebuild_start = std::time::Instant::now();
1313 let rebuilt = crate::gpu_kernels::arrow_schur::ResidentArrowFrameHandle::new(
1314 residual, ridge_t, ridge_beta, None,
1315 );
1316 accounting.frame_build_seconds += rebuild_start.elapsed().as_secs_f64();
1317 match rebuilt {
1318 Ok(frame) => {
1319 frames.frame_builds += 1;
1320 accounting.frame_builds += 1;
1321 gam_gpu::profile::telemetry_record_handle_creation(self.context_id());
1322 gam_gpu::profile::telemetry_record_factorization();
1323 gam_gpu::profile::telemetry_record_h2d(self.frame_upload_bytes());
1324 frames.keyed = Some((ridge_t, ridge_beta, frame));
1325 }
1326 Err(err) => return Err(map_gpu_error(err)),
1327 }
1328 match frames.keyed.as_ref() {
1329 Some((_, _, frame)) => {
1330 gam_gpu::profile::telemetry_record_h2d(gradient_bytes);
1331 gam_gpu::profile::telemetry_record_kernel_launch();
1332 gam_gpu::profile::telemetry_record_d2h(readback_bytes);
1333 frame.solve_gradient(&g_t, &g_beta).map_err(map_gpu_error)
1334 }
1335 None => Err(DeviceResidentArrowError::Solve {
1336 reason: "SAE resident frame build reported success without storing a frame"
1337 .to_string(),
1338 }),
1339 }
1340 }
1341}
1342
1343/// Upper bound on the number of row chunks the host operator apply folds. Fixes
1344/// the `cross_beta` reduction tree (and hence the exact floating-point result)
1345/// as a function of `n` alone, and bounds the partial-buffer memory at
1346/// `OPERATOR_MAX_ROW_CHUNKS · p` regardless of row count.
1347const OPERATOR_MAX_ROW_CHUNKS: usize = 256;
1348
1349/// Smallest row chunk worth handing to a worker: below this the per-chunk
1350/// `p`-wide partial buffer costs more than the rows it folds.
1351const OPERATOR_MIN_ROW_CHUNK: usize = 64;
1352
1353/// Row count below which the host operator apply stays sequential — the fan-out
1354/// and the per-chunk partial allocation dominate the contraction itself.
1355const OPERATOR_PARALLEL_ROW_MIN: usize = 256;
1356
1357/// Where one [`ArrowOperatorApply`] ran and what it moved across the bus.
1358#[derive(Clone, Copy, Debug, Default)]
1359struct OperatorApplyCost {
1360 on_device: bool,
1361 host_to_device_bytes: usize,
1362 device_to_host_bytes: usize,
1363}
1364
1365/// Running residency accounting for one inner Newton loop.
1366///
1367/// The fit reports its OWN split rather than leaving it to an external profiler:
1368/// #1017's flat tier landed the same discipline (`device_refresh_columns`), and
1369/// #2393's utilization gate is unfalsifiable without it. `operator_seconds`
1370/// covers the `O(n·d·p + p²)` contraction (host or device), `solve_seconds`
1371/// covers the arrow factor/solve, and the byte counters are the ONLY traffic the
1372/// resident loop generates after the one-time slab upload.
1373#[derive(Default)]
1374struct ResidencyAccounting {
1375 operator_applies: usize,
1376 operator_device_applies: usize,
1377 operator_seconds: f64,
1378 solve_seconds: f64,
1379 frame_build_seconds: f64,
1380 frame_builds: usize,
1381 base_builds: usize,
1382 base_refactors: usize,
1383 base_gradient_solves: usize,
1384 host_to_device_bytes: usize,
1385 device_to_host_bytes: usize,
1386}
1387
1388impl ResidencyAccounting {
1389 fn apply(
1390 &mut self,
1391 workspace: &DeviceResidentArrowWorkspace,
1392 mode: InnerSolveMode,
1393 t: &[f64],
1394 beta: &[f64],
1395 ) -> ArrowOperatorApply {
1396 let start = std::time::Instant::now();
1397 let (apply, cost) = workspace.apply_operator(mode.operator_on_device(), t, beta);
1398 self.operator_seconds += start.elapsed().as_secs_f64();
1399 self.operator_applies += 1;
1400 if cost.on_device {
1401 self.operator_device_applies += 1;
1402 }
1403 self.host_to_device_bytes += cost.host_to_device_bytes;
1404 self.device_to_host_bytes += cost.device_to_host_bytes;
1405 apply
1406 }
1407
1408 fn finish(self) -> ResidencyReport {
1409 ResidencyReport {
1410 operator_applies: self.operator_applies,
1411 operator_device_applies: self.operator_device_applies,
1412 operator_seconds: self.operator_seconds,
1413 solve_seconds: self.solve_seconds,
1414 frame_build_seconds: self.frame_build_seconds,
1415 frame_builds: self.frame_builds,
1416 base_builds: self.base_builds,
1417 base_refactors: self.base_refactors,
1418 base_gradient_solves: self.base_gradient_solves,
1419 operator_host_to_device_bytes: self.host_to_device_bytes,
1420 operator_device_to_host_bytes: self.device_to_host_bytes,
1421 }
1422 }
1423}
1424
1425/// Honest per-fit residency accounting, reported BY the fit (#1017/#2393).
1426///
1427/// A resident inner Newton is only resident if the per-iterate traffic is
1428/// `O(n·d + p)` and the `O(n·d·p)` operands stay put; these counters make that
1429/// claim checkable from a run's own output instead of from a profiler that a
1430/// GPU-less future session cannot rerun.
1431#[derive(Clone, Copy, Debug, Default, PartialEq)]
1432pub struct ResidencyReport {
1433 /// Operator applies (`H·z` contractions) the loop performed.
1434 pub operator_applies: usize,
1435 /// How many of those ran on the device.
1436 pub operator_device_applies: usize,
1437 /// Wall seconds inside the operator applies.
1438 pub operator_seconds: f64,
1439 /// Wall seconds inside the per-iterate arrow factor/solve. Includes
1440 /// [`Self::frame_build_seconds`]: the build happens inside the solve region,
1441 /// and subtracting gives the gradient-only remainder.
1442 pub solve_seconds: f64,
1443 /// Wall seconds of the above spent BUILDING frames rather than solving with
1444 /// them (#2539) — base-block upload, on-device re-factor, and the legacy
1445 /// host re-pack + full re-upload. Residency's whole claim is that this
1446 /// shrinks to one base build per loop; without the split the claim is
1447 /// checkable only from an external profiler on a card nobody can rerun.
1448 pub frame_build_seconds: f64,
1449 /// Host re-pack + full re-upload frame builds — the pre-#2539 path. `0` is
1450 /// the residency contract on a CUDA host; a nonzero count means the base
1451 /// frame was declined and every ridge change re-crossed the bus.
1452 pub frame_builds: usize,
1453 /// Base-block uploads. At most `1` per inner loop.
1454 pub base_builds: usize,
1455 /// On-device re-factors from the resident base blocks — one per ridge change.
1456 pub base_refactors: usize,
1457 /// Iterates served from already-built factors with no POTRF: gradient up,
1458 /// step down, nothing else.
1459 pub base_gradient_solves: usize,
1460 /// Host→device bytes the operator applies moved (the iterate only).
1461 pub operator_host_to_device_bytes: usize,
1462 /// Device→host bytes the operator applies moved (the products only).
1463 pub operator_device_to_host_bytes: usize,
1464}
1465
1466impl ResidencyReport {
1467
1468}
1469
1470/// Options for the device-resident inner Newton loop. Defaults mirror the
1471/// production [`crate::latent_inner::LatentInnerOptions`] trust-region
1472/// schedule so device and CPU paths run identical host-side control flow.
1473#[derive(Clone, Copy, Debug)]
1474pub struct DeviceResidentInnerOptions {
1475 pub max_iterations: usize,
1476 pub convergence_tolerance: f64,
1477 pub initial_ridge_t: f64,
1478 pub initial_ridge_beta: f64,
1479 pub lm_grow: f64,
1480 pub lm_shrink: f64,
1481 pub max_ridge: f64,
1482}
1483
1484impl Default for DeviceResidentInnerOptions {
1485 fn default() -> Self {
1486 Self {
1487 max_iterations: 16,
1488 convergence_tolerance: 1e-9,
1489 initial_ridge_t: 0.0,
1490 initial_ridge_beta: 0.0,
1491 lm_grow: 4.0,
1492 lm_shrink: 0.5,
1493 max_ridge: 1e9,
1494 }
1495 }
1496}
1497
1498/// Result of the full device-resident inner Newton loop.
1499#[derive(Clone, Debug)]
1500pub struct DeviceResidentInnerOutcome {
1501 pub t: Array1<f64>,
1502 pub beta: Array1<f64>,
1503 pub objective: f64,
1504 pub gradient_norm: f64,
1505 pub log_det_hessian: f64,
1506 pub iterations: usize,
1507 pub accepted_iterations: usize,
1508 pub converged: bool,
1509 pub execution_path: ExecutionPath,
1510 /// Where this fit's per-iterate work ran and what it moved (#2393).
1511 pub residency: ResidencyReport,
1512}
1513
1514/// Result of an across-outer resident sweep (`DeviceResidentArrowWorkspace::device_fit_outer_sequence`).
1515///
1516/// `outers` holds one inner-loop outcome per outer evaluation, in input order.
1517/// `frame_builds` is the total number of resident-frame (re)builds performed
1518/// across the whole sweep: for an unchanged operator with a well-posed ridge it
1519/// is exactly `1` (the across-outer amortization #1017 deliverable 3 buys —
1520/// factor once, reuse the device factors for every outer), regardless of how
1521/// many outers ran. A value `> 1` means a per-outer ridge escalation forced a
1522/// refactor, which the parity oracle still matches but which costs the
1523/// amortization for those outers.
1524#[derive(Clone, Debug)]
1525pub struct OuterSequenceOutcome {
1526 pub outers: Vec<DeviceResidentInnerOutcome>,
1527 pub frame_builds: usize,
1528 /// #2539: base-block uploads across the sweep — at most `1`, because the
1529 /// ridge-independent `D`/`B`/`H_ββ` blocks are the same system for every
1530 /// outer at an unchanged operator.
1531 pub base_builds: usize,
1532 /// #2539: on-device re-factors from the resident base blocks. This is the
1533 /// count that used to arrive as `frame_builds`: before the two-level split a
1534 /// ridge change re-packed the blocks on the host and re-uploaded them, so
1535 /// every one of these was a full rebuild.
1536 pub base_refactors: usize,
1537 /// #2539: solves that hit the ridge-keyed factors and therefore ran NO
1538 /// factor work at all — only the gradient crossed the bus.
1539 ///
1540 /// This is the counter that separates the two-level frame from a plain
1541 /// base-frame adoption. A base frame used on its own re-factors on every
1542 /// iterate, so this stays `0` and `base_refactors` equals the iterate count;
1543 /// with the factors cached on top, an unchanged-ridge iterate lands here
1544 /// instead. A trajectory whose ridge never moves must show
1545 /// `base_refactors == 1`.
1546 pub base_gradient_solves: usize,
1547}
1548
1549/// The two-level device frame carried through one inner Newton loop — and, for
1550/// `device_fit_outer_sequence`, across every outer in the sweep (#1017
1551/// deliverable 3, #2539).
1552///
1553/// `keyed` is the original ridge-keyed frame: one `(ridge_t, ridge_beta)` baked
1554/// into its factors, serving cheap no-POTRF re-solves for a fresh gradient at
1555/// that same ridge. `base` holds the ridge-INDEPENDENT blocks (`D = H_tt`,
1556/// `B = H_tβ`, border `H_ββ`) resident and re-factors on-device at any ridge, so
1557/// the `n·d×p` slabs cross the bus once per loop rather than once per ridge
1558/// change.
1559///
1560/// The counters are integers, not timings, which is what makes the amortization
1561/// falsifiable on a contended card:
1562///
1563/// * `frame_builds` — host re-pack + full re-upload frame builds. `1` on an
1564/// all-accept trajectory at `initial_ridge_t = 0`, and after #2539 it should
1565/// stay `1` even when the loop rejects, because a ridge change no longer
1566/// routes here.
1567/// * `base_builds` — base-block uploads. At most `1` per loop.
1568/// * `base_refactors` — on-device re-factors from the resident base blocks.
1569/// This is the count that used to be `frame_builds`.
1570#[derive(Default)]
1571struct SharedFrameState {
1572 keyed: Option<(
1573 f64,
1574 f64,
1575 crate::gpu_kernels::arrow_schur::ResidentArrowFrameHandle,
1576 )>,
1577 base: Option<crate::gpu_kernels::arrow_schur::ResidentBaseArrowFrameHandle>,
1578 /// Set once the base frame construction has been refused on this host, so a
1579 /// loop that cannot hold the base blocks stops re-attempting them on every
1580 /// ridge change and takes the pre-#2539 rebuild instead.
1581 base_declined: bool,
1582 /// The ridge-keyed factors living ON TOP of `base` (#2539). A same-ridge
1583 /// iterate re-solves against these and runs no POTRF; a ridge change
1584 /// replaces them from blocks that never leave the device.
1585 base_keyed: Option<crate::gpu_kernels::arrow_schur::ResidentBaseRidgeFactorsHandle>,
1586 frame_builds: usize,
1587 base_builds: usize,
1588 base_refactors: usize,
1589 base_gradient_solves: usize,
1590}
1591
1592/// One production-structured Arrow-Schur Newton step on the HOST.
1593///
1594/// `gpu_policy = Off` is what makes this a CPU baseline rather than a device
1595/// path wearing a CPU label: `solve_arrow_newton_step_core` will otherwise
1596/// consult `try_device_arrow_direct` / `maybe_inject_gpu_schur_matvec` and route
1597/// part of the solve to the device on a CUDA host, which would silently make the
1598/// "CPU" side of a speedup ratio partly a GPU measurement.
1599fn cpu_arrow_newton_step(
1600 sys: &ArrowSchurSystem,
1601 ridge_t: f64,
1602 ridge_beta: f64,
1603) -> Result<crate::gpu_kernels::arrow_schur::ArrowSchurGpuSolution, DeviceResidentArrowError> {
1604 let mut options = crate::arrow_schur::ArrowSolveOptions::direct();
1605 options.gpu_policy = gam_gpu::GpuPolicy::Off;
1606 let (delta_t, delta_beta, _diagnostics) = sys
1607 .solve_with_options(ridge_t, ridge_beta, &options)
1608 .map_err(|err| DeviceResidentArrowError::Solve {
1609 reason: format!("SAE CPU arrow baseline step failed: {err}"),
1610 })?;
1611 Ok(crate::gpu_kernels::arrow_schur::ArrowSchurGpuSolution {
1612 delta_t,
1613 delta_beta,
1614 log_det_hessian: f64::NAN,
1615 })
1616}
1617
1618fn grow_ridge(current: f64, grow: f64) -> f64 {
1619 if current == 0.0 { 1e-6 } else { current * grow }
1620}
1621
1622fn arrow_system_gradient_norm(sys: &ArrowSchurSystem) -> f64 {
1623 let mut acc = 0.0_f64;
1624 for row in &sys.rows {
1625 for &v in row.gt.iter() {
1626 acc += v * v;
1627 }
1628 }
1629 for &v in sys.gb.iter() {
1630 acc += v * v;
1631 }
1632 acc.sqrt()
1633}
1634
1635fn iterate_norm(t: &[f64], beta: &[f64]) -> f64 {
1636 (squared_norm(t) + squared_norm(beta)).sqrt()
1637}
1638
1639fn validate_shape(
1640 shape: DeviceResidentArrowShape,
1641 target_x: &[f64],
1642 basis_values: &[f64],
1643 gate_activations: &[f64],
1644 slabs: &DeviceResidentArrowSlabs,
1645) -> Result<(), DeviceResidentArrowError> {
1646 let checks = [
1647 ("target_x", target_x.len(), shape.target_len()),
1648 ("basis_values", basis_values.len(), shape.basis_len()),
1649 (
1650 "gate_activations",
1651 gate_activations.len(),
1652 shape.basis_len(),
1653 ),
1654 (
1655 "row_hessian_slabs",
1656 slabs.row_hessian_slabs.len(),
1657 shape.row_hessian_len(),
1658 ),
1659 (
1660 "row_cross_slabs",
1661 slabs.row_cross_slabs.len(),
1662 shape.row_cross_len(),
1663 ),
1664 (
1665 "row_gradient_slabs",
1666 slabs.row_gradient_slabs.len(),
1667 shape.row_gradient_len(),
1668 ),
1669 (
1670 "border_hessian",
1671 slabs.border_hessian.len(),
1672 shape.border_hessian_len(),
1673 ),
1674 ("border_gradient", slabs.border_gradient.len(), shape.p),
1675 ];
1676 for (label, got, want) in checks {
1677 if got != want {
1678 return Err(DeviceResidentArrowError::Shape {
1679 reason: format!(
1680 "SAE resident workspace shape mismatch for {label}: got {got}, expected {want}"
1681 ),
1682 });
1683 }
1684 }
1685 if shape.n == 0 || shape.p == 0 || shape.d == 0 || shape.basis_cols == 0 {
1686 return Err(DeviceResidentArrowError::Shape {
1687 reason: "SAE resident workspace requires nonzero n, p, basis_cols, and d".to_string(),
1688 });
1689 }
1690 Ok(())
1691}
1692
1693#[cfg(target_os = "linux")]
1694fn upload_resident_buffers(
1695 shape: DeviceResidentArrowShape,
1696 target_x: &[f64],
1697 basis_values: &[f64],
1698 gate_activations: &[f64],
1699 slabs: &DeviceResidentArrowSlabs,
1700) -> Option<DeviceResidentArrowBuffers> {
1701 use gam_gpu::linalg_dispatch::{DispatchOp, route_through_gpu};
1702
1703 // Admission is the batched `d×d` POTRF over the n row blocks, with the
1704 // reduced-Schur GEMM `p × p × (n·d)` — the op the frame build is actually
1705 // dominated by — as the alternate route.
1706 //
1707 // Keying admission on the GEMM ALONE was tried and rejected on measurement
1708 // (#2393): the A10 sweep appeared to show the resident path losing at
1709 // p=128 (0.56× vs the production host solve), which would have justified a
1710 // narrower gate, but the whole deficit was the FIRST device call in the
1711 // process paying cuBLAS/context warm-up inside the timed region — 39.5 ms
1712 // at p=128 against 0.5–2.5 ms steady-state at every larger border. Warmed,
1713 // the device wins at every border width measured, so a gate that excluded
1714 // small `p` would have been tuned to a startup artifact.
1715 let runtime = route_through_gpu(DispatchOp::SmallDenseBatchedPotrf {
1716 p: shape.d,
1717 batch: shape.n,
1718 })
1719 .or_else(|| {
1720 route_through_gpu(DispatchOp::Gemm {
1721 m: shape.p,
1722 n: shape.p,
1723 k: shape.n * shape.d,
1724 })
1725 })?;
1726 let ctx = gam_gpu::device_runtime::cuda_context_for(runtime.device.ordinal)?;
1727 let stream = ctx.new_stream().ok()?;
1728 let target_x_dev = stream.clone_htod(target_x).ok()?;
1729 let basis_values_dev = stream.clone_htod(basis_values).ok()?;
1730 let gate_activations_dev = stream.clone_htod(gate_activations).ok()?;
1731 let row_hessian_dev = stream.clone_htod(&slabs.row_hessian_slabs).ok()?;
1732 let row_cross_dev = stream.clone_htod(&slabs.row_cross_slabs).ok()?;
1733 let row_gradient_dev = stream.clone_htod(&slabs.row_gradient_slabs).ok()?;
1734 let border_hessian_dev = stream.clone_htod(&slabs.border_hessian).ok()?;
1735 let border_gradient_dev = stream.clone_htod(&slabs.border_gradient).ok()?;
1736 // One cuBLAS handle + one set of O(n·d + p) scratch buffers per workspace,
1737 // created with the resident slabs and reused for every operator apply.
1738 let blas = cudarc::cublas::CudaBlas::new(stream.clone()).ok()?;
1739 let rows_times_d = shape.n.checked_mul(shape.d)?;
1740 let operator_scratch = std::sync::Mutex::new(DeviceOperatorScratch {
1741 t_dev: stream.alloc_zeros::<f64>(rows_times_d).ok()?,
1742 beta_dev: stream.alloc_zeros::<f64>(shape.p).ok()?,
1743 cross_t_dev: stream.alloc_zeros::<f64>(rows_times_d).ok()?,
1744 cross_beta_dev: stream.alloc_zeros::<f64>(shape.p).ok()?,
1745 border_beta_dev: stream.alloc_zeros::<f64>(shape.p).ok()?,
1746 });
1747 let bytes = [
1748 target_x.len(),
1749 basis_values.len(),
1750 gate_activations.len(),
1751 slabs.row_hessian_slabs.len(),
1752 slabs.row_cross_slabs.len(),
1753 slabs.row_gradient_slabs.len(),
1754 slabs.border_hessian.len(),
1755 slabs.border_gradient.len(),
1756 ]
1757 .into_iter()
1758 .sum::<usize>()
1759 * std::mem::size_of::<f64>();
1760 Some(DeviceResidentArrowBuffers {
1761 stream,
1762 target_x_dev,
1763 basis_values_dev,
1764 gate_activations_dev,
1765 row_hessian_dev,
1766 row_cross_dev,
1767 row_gradient_dev,
1768 border_hessian_dev,
1769 border_gradient_dev,
1770 bytes,
1771 blas,
1772 operator_scratch,
1773 })
1774}
1775
1776fn map_gpu_error(err: ArrowSchurGpuFailure) -> DeviceResidentArrowError {
1777 match err {
1778 ArrowSchurGpuFailure::Unavailable => DeviceResidentArrowError::Unavailable {
1779 reason: "SAE resident inner iteration unavailable after GPU admission".to_string(),
1780 },
1781 ArrowSchurGpuFailure::RidgeBumpRequired { row, bump } => DeviceResidentArrowError::Solve {
1782 reason: format!("SAE resident inner iteration row {row} requires ridge bump {bump:e}"),
1783 },
1784 ArrowSchurGpuFailure::SchurFactorFailed { reason } => {
1785 DeviceResidentArrowError::Solve { reason }
1786 }
1787 ArrowSchurGpuFailure::GpuRequiresDenseSystem {
1788 had_hbb_matvec,
1789 had_htbeta_matvec,
1790 } => DeviceResidentArrowError::Solve {
1791 reason: format!(
1792 "SAE resident inner iteration requires dense slabs; hbb_matvec={had_hbb_matvec} htbeta_matvec={had_htbeta_matvec}"
1793 ),
1794 },
1795 }
1796}
1797
1798fn squared_norm(values: &[f64]) -> f64 {
1799 values.iter().map(|v| v * v).sum()
1800}
1801
1802impl From<ArrowSchurError> for DeviceResidentArrowError {
1803 fn from(err: ArrowSchurError) -> Self {
1804 Self::Solve {
1805 reason: err.to_string(),
1806 }
1807 }
1808}
1809
1810/// Deterministic qwen-scale non-gating fixture for the resident harness.
1811pub fn qwen_non_gating_fixture() -> Result<DeviceResidentArrowWorkspace, DeviceResidentArrowError> {
1812 qwen_non_gating_fixture_seeded(0x1017_0003_D3A1_5EED)
1813}
1814
1815/// Seeded variant of [`qwen_non_gating_fixture`]. Distinct seeds produce
1816/// distinct-but-well-conditioned resident frames, used to build independent
1817/// replicate fits for the stream-multiplexing parity harness.
1818pub fn qwen_non_gating_fixture_seeded(
1819 seed: u64,
1820) -> Result<DeviceResidentArrowWorkspace, DeviceResidentArrowError> {
1821 fixture_for_shape_seeded(DeviceResidentArrowShape::qwen_non_gating(), seed)
1822}
1823
1824/// Deterministic color-arm-scale resident fixture (n=180, p=5120) for the
1825/// #1017 GPU wall-clock bench: few rows, very wide border — the shape where the
1826/// per-iterate re-upload + re-factor that across-iteration residency eliminates
1827/// dominates.
1828pub fn color_arm_fixture() -> Result<DeviceResidentArrowWorkspace, DeviceResidentArrowError> {
1829 fixture_for_shape_seeded(DeviceResidentArrowShape::color_arm(), 0x1017_C010_2A12_5EED)
1830}
1831
1832/// Build a well-conditioned resident frame for any `d == 2` shape. Both the
1833/// qwen and color-arm fixtures share this body; the conditioning (strong row
1834/// `H_tt` diagonals, tiny cross blocks, diagonally-dominant border) keeps the
1835/// dense reference factorisation PD so the parity harness is meaningful.
1836fn fixture_for_shape_seeded(
1837 shape: DeviceResidentArrowShape,
1838 seed: u64,
1839) -> Result<DeviceResidentArrowWorkspace, DeviceResidentArrowError> {
1840 if shape.d == 0 {
1841 return Err(DeviceResidentArrowError::Shape {
1842 reason: "fixture_for_shape_seeded requires d >= 1".to_string(),
1843 });
1844 }
1845 let d = shape.d;
1846 let mut rng = SplitMix64::new(seed);
1847 let mut target_x = vec![0.0_f64; shape.target_len()];
1848 for i in 0..shape.n {
1849 for j in 0..shape.p {
1850 let phase = ((i % 97) as f64) * 0.013 + ((j % 131) as f64) * 0.007;
1851 target_x[i * shape.p + j] = 0.02 * phase.sin() + 0.001 * rng.sample_signed();
1852 }
1853 }
1854 let mut basis_values = vec![0.0_f64; shape.basis_len()];
1855 let mut gate_activations = vec![1.0_f64; shape.basis_len()];
1856 for i in 0..shape.n {
1857 for a in 0..shape.basis_cols {
1858 let phase = ((i + 1) as f64) * ((a + 1) as f64) * 0.003;
1859 basis_values[i * shape.basis_cols + a] = phase.cos();
1860 gate_activations[i * shape.basis_cols + a] = 1.0;
1861 }
1862 }
1863 let mut row_hessian_slabs = vec![0.0_f64; shape.row_hessian_len()];
1864 let mut row_cross_slabs = vec![0.0_f64; shape.row_cross_len()];
1865 let mut row_gradient_slabs = vec![0.0_f64; shape.row_gradient_len()];
1866 for i in 0..shape.n {
1867 let mut basis_sum = 0.0_f64;
1868 for a in 0..shape.basis_cols {
1869 basis_sum +=
1870 basis_values[i * shape.basis_cols + a] * gate_activations[i * shape.basis_cols + a];
1871 }
1872 // Strongly diagonally-dominant d×d H_tt (row-major): diagonal ≈ 3, tiny
1873 // symmetric off-diagonals — PD for any d so the dense reference factors.
1874 let h_base = i * d * d;
1875 for r in 0..d {
1876 for c in 0..d {
1877 let v = if r == c {
1878 3.0 + 0.01 * basis_sum.abs() + 0.1 * (r as f64)
1879 } else {
1880 0.02 * (basis_sum + (r + c) as f64).sin() / (d as f64)
1881 };
1882 row_hessian_slabs[h_base + r * d + c] = v;
1883 }
1884 }
1885 // Symmetrize the off-diagonals exactly.
1886 for r in 0..d {
1887 for c in 0..r {
1888 let avg = 0.5
1889 * (row_hessian_slabs[h_base + r * d + c]
1890 + row_hessian_slabs[h_base + c * d + r]);
1891 row_hessian_slabs[h_base + r * d + c] = avg;
1892 row_hessian_slabs[h_base + c * d + r] = avg;
1893 }
1894 }
1895 // d×p cross block (row-major) and length-d gradient.
1896 let b_base = i * d * shape.p;
1897 let g_base = i * d;
1898 for r in 0..d {
1899 for j in 0..shape.p {
1900 let feature = ((j % 257) as f64) * 0.011;
1901 row_cross_slabs[b_base + r * shape.p + j] =
1902 1.0e-4 * (basis_sum + r as f64).sin() * feature.cos();
1903 }
1904 row_gradient_slabs[g_base + r] = 0.01 * (basis_sum + r as f64).sin();
1905 }
1906 }
1907 let mut border_hessian = vec![0.0_f64; shape.border_hessian_len()];
1908 for r in 0..shape.p {
1909 border_hessian[r * shape.p + r] = 4.0;
1910 if r + 1 < shape.p {
1911 border_hessian[r * shape.p + r + 1] = 0.01;
1912 border_hessian[(r + 1) * shape.p + r] = 0.01;
1913 }
1914 }
1915 let mut border_gradient = vec![0.0_f64; shape.p];
1916 for j in 0..shape.p {
1917 border_gradient[j] = 0.001 * ((j % 193) as f64 * 0.017).sin();
1918 }
1919 DeviceResidentArrowWorkspace::new(
1920 shape,
1921 target_x,
1922 basis_values,
1923 gate_activations,
1924 DeviceResidentArrowSlabs {
1925 row_hessian_slabs,
1926 row_cross_slabs,
1927 row_gradient_slabs,
1928 border_hessian,
1929 border_gradient,
1930 },
1931 )
1932}
1933
1934/// One multiplexed resident fit: the workspace plus the inner-loop outcome.
1935pub struct MultiplexedFit {
1936 pub outcome: DeviceResidentInnerOutcome,
1937}
1938
1939/// Phase 4: run `workspaces.len()` independent device-resident inner fits that
1940/// share one device.
1941///
1942/// # Stream-multiplexing safety argument
1943///
1944/// Each fit calls [`DeviceResidentArrowWorkspace::device_fit`], whose per-row
1945/// arrow solve (`solve_arrow_newton_step`) acquires the **process-shared**
1946/// `Arc<CudaContext>` via `device_runtime::cuda_context_for` (a `Mutex`-guarded
1947/// `OnceLock` cache) and then creates its **own** `CudaStream` with its own
1948/// cuSOLVER/cuBLAS handles and its own device allocations. Distinct streams off
1949/// one shared context execute concurrently on the device; the only shared
1950/// mutable state — the context cache and cudarc's allocator — is internally
1951/// synchronised, and no two fits touch the same stream, handle, or buffer. So
1952/// independent fits are data-race-free and the device serialises only where the
1953/// hardware must (shared SMs / copy engines), which is exactly the throughput
1954/// multiplexing the issue's Phase 4 calls for.
1955///
1956/// Concurrency is driven through `run_topology_race_parallel` (bac4af426),
1957/// which already bounds nested Rayon so each fit's internal `par_iter`/faer
1958/// parallelism stays inside its per-fit thread budget rather than oversubscribing
1959/// the global pool. Results are returned in input order. A single A100 thus hosts
1960/// many color-/qwen-arm fits at once — the cross-fit batch where the 1e5–1e6×
1961/// race speedup materialises.
1962///
1963/// The process-wide typed GPU availability cache and per-ordinal context
1964/// cache are warmed by constructing the resident workspaces (each `new` calls
1965/// the same probe), so the per-fit calls inside the Rayon scope only *read* the
1966/// already-initialised `OnceLock`s — they never trigger a `get_or_init` whose
1967/// closure does nested parallel work, avoiding the OnceLock×Rayon deadlock.
1968pub fn run_resident_fits_multiplexed(
1969 workspaces: Vec<DeviceResidentArrowWorkspace>,
1970 opts: DeviceResidentInnerOptions,
1971) -> Result<Vec<Result<MultiplexedFit, DeviceResidentArrowError>>, String> {
1972 run_resident_fits_multiplexed_with(workspaces, opts, |workspace, opts| {
1973 workspace.device_fit(opts)
1974 })
1975}
1976
1977/// Multiplexing core parameterised over the per-fit runner, so the CPU-reference
1978/// path can exercise the exact same `run_topology_race_parallel` plumbing as the
1979/// device path in tests that run without CUDA.
1980fn run_resident_fits_multiplexed_with<Run>(
1981 workspaces: Vec<DeviceResidentArrowWorkspace>,
1982 opts: DeviceResidentInnerOptions,
1983 run_one: Run,
1984) -> Result<Vec<Result<MultiplexedFit, DeviceResidentArrowError>>, String>
1985where
1986 Run: Fn(
1987 &DeviceResidentArrowWorkspace,
1988 &DeviceResidentInnerOptions,
1989 ) -> Result<DeviceResidentInnerOutcome, DeviceResidentArrowError>
1990 + Sync,
1991{
1992 let rows = crate::topology_selector::run_topology_race_parallel(
1993 workspaces,
1994 move |workspace: DeviceResidentArrowWorkspace| {
1995 run_one(&workspace, &opts).map(|outcome| MultiplexedFit { outcome })
1996 },
1997 )?;
1998 Ok(rows.into_iter().map(|row| row.result).collect())
1999}
2000
2001/// Sequential reference for the multiplexing parity harness: the same fits run
2002/// one after another on the same shared device. Multiplexed results must be
2003/// bit-identical to this because each fit's arithmetic is independent of the
2004/// others — sharing the device changes only scheduling, never the numbers.
2005pub fn run_resident_fits_sequential(
2006 workspaces: &[DeviceResidentArrowWorkspace],
2007 opts: &DeviceResidentInnerOptions,
2008) -> Vec<Result<MultiplexedFit, DeviceResidentArrowError>> {
2009 workspaces
2010 .iter()
2011 .map(|workspace| {
2012 workspace
2013 .device_fit(opts)
2014 .map(|outcome| MultiplexedFit { outcome })
2015 })
2016 .collect()
2017}
2018
2019// ---------------------------------------------------------------------------
2020// Phase 4 variant sweep (#1017): the OLMo research battery's independent-fit
2021// matrix (K × topology × basis × layer/checkpoint) dispatched concurrently on
2022// one device.
2023//
2024// Each variant is a SEPARATE fit with its OWN resident frame: the per-fit
2025// arithmetic is independent of the others, so multiplexing them onto one a100
2026// changes only scheduling, never the numbers. This is the cross-fit batch where
2027// the issue's 1e5–1e6× race throughput materialises — and unlike per-fit
2028// across-iteration residency it needs NO fixed-quadratic inner loop, because the
2029// parallelism is BETWEEN fits, not within one.
2030// ---------------------------------------------------------------------------
2031
2032/// One independent fit in the battery's variant sweep. The battery maps each
2033/// (K, topology, basis, layer, checkpoint, seed) cell of its matrix to a
2034/// `SweepVariant`; `dim` carries the resident-frame shape that cell produces
2035/// after the host assembles its row/border slabs. Distinct `seed`s keep the
2036/// fits genuinely independent (no shared device buffer, handle, or stream).
2037#[derive(Clone, Copy, Debug)]
2038pub struct SweepVariant {
2039 /// Resident-frame shape for this variant's frozen gate/basis frame.
2040 pub dim: DeviceResidentArrowShape,
2041 /// Deterministic seed for this variant's fixture/frame.
2042 pub seed: u64,
2043}
2044
2045/// Throughput summary for a multiplexed variant sweep on one device.
2046#[derive(Clone, Copy, Debug)]
2047pub struct SweepThroughput {
2048 pub fits: usize,
2049 pub succeeded: usize,
2050 pub wall_seconds: f64,
2051 /// Fits completed per wall-clock second on the single shared device.
2052 pub fits_per_second: f64,
2053}
2054
2055/// Build the independent resident workspaces for a variant sweep. Each variant
2056/// gets its own well-conditioned `d == 2` frame (the host feeds real slabs in
2057/// production; here the deterministic fixture stands in for the parity/throughput
2058/// harness). Returns the workspaces in variant order.
2059pub fn build_sweep_workspaces(
2060 variants: &[SweepVariant],
2061) -> Result<Vec<DeviceResidentArrowWorkspace>, DeviceResidentArrowError> {
2062 variants
2063 .iter()
2064 .map(|v| fixture_for_shape_seeded(v.dim, v.seed))
2065 .collect()
2066}
2067
2068/// Dispatch a variant sweep concurrently on one device and measure cross-fit
2069/// throughput. Returns the per-variant outcomes (in variant order) and the
2070/// throughput summary (fits/sec on the single shared a100). Per-fit certified
2071/// parity is asserted by [`assert_sweep_parity_vs_sequential`].
2072pub fn run_variant_sweep_multiplexed(
2073 variants: &[SweepVariant],
2074 opts: DeviceResidentInnerOptions,
2075) -> Result<
2076 (
2077 Vec<Result<MultiplexedFit, DeviceResidentArrowError>>,
2078 SweepThroughput,
2079 ),
2080 String,
2081> {
2082 let workspaces = build_sweep_workspaces(variants).map_err(|e| e.to_string())?;
2083 run_battery_sweep_multiplexed(workspaces, opts)
2084}
2085
2086/// Production battery entry (#1017 Phase 4): dispatch CALLER-ASSEMBLED resident
2087/// workspaces concurrently on one device and measure cross-fit throughput.
2088///
2089/// This is the real-slab seam the OLMo battery uses: the host (pyffi) builds one
2090/// [`DeviceResidentArrowWorkspace`] per matrix cell from the cell's ACTUAL SAE
2091/// row_hessian/row_cross/border slabs via [`DeviceResidentArrowWorkspace::new`],
2092/// then hands the workspaces here. Unlike [`run_variant_sweep_multiplexed`]
2093/// (which builds frames from the deterministic harness fixture), this consumes
2094/// real frames, so the printed throughput is the battery's true fits/sec on one
2095/// device. Returns per-cell outcomes (in input order) + the throughput summary.
2096pub fn run_battery_sweep_multiplexed(
2097 workspaces: Vec<DeviceResidentArrowWorkspace>,
2098 opts: DeviceResidentInnerOptions,
2099) -> Result<
2100 (
2101 Vec<Result<MultiplexedFit, DeviceResidentArrowError>>,
2102 SweepThroughput,
2103 ),
2104 String,
2105> {
2106 let fits = workspaces.len();
2107 let start = std::time::Instant::now();
2108 let results = run_resident_fits_multiplexed(workspaces, opts)?;
2109 let wall_seconds = start.elapsed().as_secs_f64();
2110 let succeeded = results.iter().filter(|r| r.is_ok()).count();
2111 let throughput = SweepThroughput {
2112 fits,
2113 succeeded,
2114 wall_seconds,
2115 fits_per_second: (fits as f64) / wall_seconds.max(1e-9),
2116 };
2117 Ok((results, throughput))
2118}
2119
2120/// The OLMo battery's full color-arm variant matrix as [`SweepVariant`]s:
2121/// `K{1..=4} × topology{4} × basis{periodic, linear}` at the color-arm shape
2122/// (n=180, p=5120). `d` and `basis_cols` follow the intrinsic-rank convention
2123/// (periodic ⇒ d=2, basis_cols=8; linear ⇒ d=1, basis_cols=2). Exposed so the
2124/// pyffi battery seam can quote cross-fit throughput on the real shape matrix
2125/// (fixture frames) before the per-cell real-slab fits are wired through.
2126#[must_use]
2127pub fn color_arm_variant_matrix() -> Vec<SweepVariant> {
2128 let topologies = ["euclidean", "circle", "torus", "sphere"];
2129 let mut variants = Vec::with_capacity(4 * topologies.len() * 2);
2130 for k in 1..=4u64 {
2131 for (t_idx, _topology) in topologies.iter().enumerate() {
2132 // periodic (2 harmonics) and linear basis arms.
2133 for &(d, basis_cols, basis_tag) in &[(2usize, 8usize, 0u64), (1usize, 2usize, 1u64)] {
2134 let mut dim = DeviceResidentArrowShape::color_arm();
2135 dim.d = d;
2136 dim.basis_cols = basis_cols;
2137 let seed = 0x1017_C010_0000_0000 ^ (k << 16) ^ ((t_idx as u64) << 8) ^ basis_tag;
2138 variants.push(SweepVariant { dim, seed });
2139 }
2140 }
2141 }
2142 variants
2143}
2144
2145/// Certified per-fit parity for a variant sweep: the multiplexed (concurrent)
2146/// results must be bit-for-bit identical to the same fits run sequentially on
2147/// the same device, because independent fits' arithmetic does not depend on
2148/// scheduling. Returns the sequential throughput so the caller can report the
2149/// multiplex speedup (multiplexed fits/sec ÷ sequential fits/sec). Returns an
2150/// `Err` describing the first divergence so the harness fails loudly.
2151pub fn assert_sweep_parity_vs_sequential(
2152 variants: &[SweepVariant],
2153 opts: &DeviceResidentInnerOptions,
2154 multiplexed: &[Result<MultiplexedFit, DeviceResidentArrowError>],
2155) -> Result<SweepThroughput, String> {
2156 let workspaces = build_sweep_workspaces(variants).map_err(|e| e.to_string())?;
2157 let start = std::time::Instant::now();
2158 let sequential = run_resident_fits_sequential(&workspaces, opts);
2159 let wall_seconds = start.elapsed().as_secs_f64();
2160 if sequential.len() != multiplexed.len() {
2161 return Err(format!(
2162 "sweep parity: length mismatch seq={} mux={}",
2163 sequential.len(),
2164 multiplexed.len()
2165 ));
2166 }
2167 for (idx, (seq, mux)) in sequential.iter().zip(multiplexed.iter()).enumerate() {
2168 match (seq, mux) {
2169 (Ok(s), Ok(m)) => {
2170 if s.outcome.t.as_slice() != m.outcome.t.as_slice()
2171 || s.outcome.beta.as_slice() != m.outcome.beta.as_slice()
2172 || s.outcome.objective.to_bits() != m.outcome.objective.to_bits()
2173 {
2174 return Err(format!(
2175 "sweep parity: fit {idx} multiplexed result differs from sequential"
2176 ));
2177 }
2178 }
2179 (Err(sequential), Err(multiplexed)) => {
2180 // Both paths refused. Parity is about agreement, and they agree
2181 // here, so this is not a parity failure - but a sweep where every
2182 // fit refuses would otherwise pass silently.
2183 log::debug!(
2184 "sweep parity: fit {idx} refused on both paths (seq: {sequential}; mux: {multiplexed})"
2185 );
2186 }
2187 _ => {
2188 return Err(format!(
2189 "sweep parity: fit {idx} success/failure disagrees seq-vs-mux"
2190 ));
2191 }
2192 }
2193 }
2194 let fits = variants.len();
2195 let succeeded = sequential.iter().filter(|r| r.is_ok()).count();
2196 Ok(SweepThroughput {
2197 fits,
2198 succeeded,
2199 wall_seconds,
2200 fits_per_second: (fits as f64) / wall_seconds.max(1e-9),
2201 })
2202}
2203
2204struct SplitMix64 {
2205 state: u64,
2206}
2207
2208impl SplitMix64 {
2209 const fn new(seed: u64) -> Self {
2210 Self { state: seed }
2211 }
2212
2213 fn next_u64(&mut self) -> u64 {
2214 gam_linalg::utils::splitmix64(&mut self.state)
2215 }
2216
2217 fn sample_signed(&mut self) -> f64 {
2218 let unit = (self.next_u64() >> 11) as f64 / ((1_u64 << 53) as f64);
2219 2.0 * unit - 1.0
2220 }
2221}
2222
2223#[cfg(test)]
2224mod tests {
2225 use super::*;
2226 use ndarray::Array2;
2227
2228 /// Build a small, strongly diagonally-dominant resident frame whose dense
2229 /// reference factorisation is well-conditioned. The objective minimiser is
2230 /// `z* = H^{-1} g₀`, which the inner loop must reach.
2231 fn small_fixture(seed: u64) -> DeviceResidentArrowWorkspace {
2232 // batch (n) = 8 clears the device dispatch floor
2233 // (`small_dense_batched_potrf_min_batch = 8`) so that on a CUDA host
2234 // `upload_resident_buffers` actually binds a device and
2235 // `device_resident()` is TRUE — otherwise the device-resident parity
2236 // branch of `device_resident_fit_matches_cpu_reference` is dead on real
2237 // GPU hardware (the route declines for batch < 8, so the test only ever
2238 // exercised the CPU-decline branch and never validated the device loop).
2239 let shape = DeviceResidentArrowShape {
2240 n: 8,
2241 p: 4,
2242 basis_cols: 2,
2243 d: 2,
2244 };
2245 let mut rng = SplitMix64::new(seed);
2246 let target_x = vec![0.0_f64; shape.target_len()];
2247 let basis_values = vec![0.5_f64; shape.basis_len()];
2248 let gate_activations = vec![1.0_f64; shape.basis_len()];
2249
2250 let mut row_hessian_slabs = vec![0.0_f64; shape.row_hessian_len()];
2251 let mut row_cross_slabs = vec![0.0_f64; shape.row_cross_len()];
2252 let mut row_gradient_slabs = vec![0.0_f64; shape.row_gradient_len()];
2253 for i in 0..shape.n {
2254 let h = i * shape.d * shape.d;
2255 row_hessian_slabs[h] = 5.0 + 0.1 * rng.sample_signed();
2256 row_hessian_slabs[h + 1] = 0.05 * rng.sample_signed();
2257 row_hessian_slabs[h + 2] = row_hessian_slabs[h + 1];
2258 row_hessian_slabs[h + 3] = 4.0 + 0.1 * rng.sample_signed();
2259 let b = i * shape.d * shape.p;
2260 for j in 0..shape.p {
2261 row_cross_slabs[b + j] = 0.01 * rng.sample_signed();
2262 row_cross_slabs[b + shape.p + j] = 0.01 * rng.sample_signed();
2263 }
2264 let g = i * shape.d;
2265 row_gradient_slabs[g] = rng.sample_signed();
2266 row_gradient_slabs[g + 1] = rng.sample_signed();
2267 }
2268 let mut border_hessian = vec![0.0_f64; shape.border_hessian_len()];
2269 for r in 0..shape.p {
2270 border_hessian[r * shape.p + r] = 6.0 + 0.1 * rng.sample_signed();
2271 }
2272 let border_gradient: Vec<f64> = (0..shape.p).map(|_| rng.sample_signed()).collect();
2273
2274 DeviceResidentArrowWorkspace::new(
2275 shape,
2276 target_x,
2277 basis_values,
2278 gate_activations,
2279 DeviceResidentArrowSlabs {
2280 row_hessian_slabs,
2281 row_cross_slabs,
2282 row_gradient_slabs,
2283 border_hessian,
2284 border_gradient,
2285 },
2286 )
2287 .expect("small resident fixture must validate")
2288 }
2289
2290 /// Dense `H z` for the resident frame (independent of the arrow path),
2291 /// used to confirm the inner-loop fixed point is the true stationary point.
2292 fn dense_hz(
2293 ws: &DeviceResidentArrowWorkspace,
2294 sys: &ArrowSchurSystem,
2295 ) -> (Array2<f64>, Array1<f64>) {
2296 let shape = ws.shape;
2297 let total = shape.n * shape.d + shape.p;
2298 let mut h = Array2::<f64>::zeros((total, total));
2299 let mut g0 = Array1::<f64>::zeros(total);
2300 for i in 0..shape.n {
2301 let base = i * shape.d;
2302 for r in 0..shape.d {
2303 for c in 0..shape.d {
2304 h[[base + r, base + c]] = sys.rows[i].htt[[r, c]];
2305 }
2306 for c in 0..shape.p {
2307 let v = sys.rows[i].htbeta[[r, c]];
2308 h[[base + r, shape.n * shape.d + c]] = v;
2309 h[[shape.n * shape.d + c, base + r]] = v;
2310 }
2311 g0[base + r] = sys.rows[i].gt[r];
2312 }
2313 }
2314 for r in 0..shape.p {
2315 for c in 0..shape.p {
2316 h[[shape.n * shape.d + r, shape.n * shape.d + c]] = sys.hbb[[r, c]];
2317 }
2318 g0[shape.n * shape.d + r] = sys.gb[r];
2319 }
2320 (h, g0)
2321 }
2322
2323 /// The fused operator apply must reproduce the dense `H·z` blockwise: it is
2324 /// the ONE contraction both the residual gradient and the objective are now
2325 /// read from, so an error here would corrupt every downstream quantity while
2326 /// still "converging".
2327 #[test]
2328 fn operator_apply_matches_dense_hessian_product() {
2329 let ws = small_fixture(0x2393_0A01);
2330 let sys = ws.to_arrow_system();
2331 let (h, _g0) = dense_hz(&ws, &sys);
2332 let shape = ws.shape;
2333 let t_len = shape.n * shape.d;
2334 let mut rng = SplitMix64::new(0x2393_0A02);
2335 let t: Vec<f64> = (0..t_len).map(|_| rng.sample_signed()).collect();
2336 let beta: Vec<f64> = (0..shape.p).map(|_| rng.sample_signed()).collect();
2337
2338 let apply = ws.apply_operator_host(&t, &beta);
2339
2340 // cross_t = H_tβ β (the t-rows of H·z minus the H_tt t part).
2341 let mut max_err = 0.0_f64;
2342 for row in 0..t_len {
2343 let mut expect = 0.0_f64;
2344 for (col, &b) in beta.iter().enumerate() {
2345 expect += h[[row, t_len + col]] * b;
2346 }
2347 max_err = max_err.max((expect - apply.cross_t[row]).abs());
2348 }
2349 // cross_beta = H_βt t, border_beta = H_ββ β.
2350 for col in 0..shape.p {
2351 let mut cross = 0.0_f64;
2352 for (row, &tv) in t.iter().enumerate() {
2353 cross += h[[t_len + col, row]] * tv;
2354 }
2355 max_err = max_err.max((cross - apply.cross_beta[col]).abs());
2356 let mut border = 0.0_f64;
2357 for (c, &b) in beta.iter().enumerate() {
2358 border += h[[t_len + col, t_len + c]] * b;
2359 }
2360 max_err = max_err.max((border - apply.border_beta[col]).abs());
2361 }
2362 assert!(
2363 max_err < 1e-12,
2364 "fused operator apply disagrees with the dense H·z blocks: max_abs_err={max_err:e}"
2365 );
2366 }
2367
2368 /// The residual written by `residual_into` must equal `H z − g₀` from the
2369 /// dense operator, and the objective read off the same apply must equal the
2370 /// dense bordered quadratic. This pins the algebra that replaced the
2371 /// per-iteration `to_arrow_system()` rebuild.
2372 #[test]
2373 fn residual_and_objective_from_apply_match_dense_quadratic() {
2374 let ws = small_fixture(0x2393_0B01);
2375 let base = ws.to_arrow_system();
2376 let (h, g0) = dense_hz(&ws, &base);
2377 let shape = ws.shape;
2378 let t_len = shape.n * shape.d;
2379 let total = t_len + shape.p;
2380 let mut rng = SplitMix64::new(0x2393_0B02);
2381 let t: Vec<f64> = (0..t_len).map(|_| rng.sample_signed()).collect();
2382 let beta: Vec<f64> = (0..shape.p).map(|_| rng.sample_signed()).collect();
2383 let z: Vec<f64> = t.iter().chain(beta.iter()).copied().collect();
2384
2385 let apply = ws.apply_operator_host(&t, &beta);
2386 let mut residual = ws.to_arrow_system();
2387 ws.residual_into(&mut residual, &base, &apply, &t);
2388
2389 let mut max_err = 0.0_f64;
2390 for row in 0..total {
2391 let mut hz = 0.0_f64;
2392 for (col, &value) in z.iter().enumerate() {
2393 hz += h[[row, col]] * value;
2394 }
2395 let want = hz - g0[row];
2396 let got = if row < t_len {
2397 residual.rows[row / shape.d].gt[row % shape.d]
2398 } else {
2399 residual.gb[row - t_len]
2400 };
2401 max_err = max_err.max((want - got).abs());
2402 }
2403 assert!(
2404 max_err < 1e-12,
2405 "residual_into disagrees with dense H z − g₀: max_abs_err={max_err:e}"
2406 );
2407
2408 let half_target_energy = 0.5 * squared_norm(&ws.target_x);
2409 let mut quad = 0.0_f64;
2410 let mut lin = 0.0_f64;
2411 for (row, &zr) in z.iter().enumerate() {
2412 let mut hz = 0.0_f64;
2413 for (col, &zc) in z.iter().enumerate() {
2414 hz += h[[row, col]] * zc;
2415 }
2416 quad += zr * hz;
2417 lin += g0[row] * zr;
2418 }
2419 let want = half_target_energy + 0.5 * quad - lin;
2420 let got = ws.objective_from_apply(&base, half_target_energy, &apply, &t, &beta);
2421 let rel = (want - got).abs() / want.abs().max(1.0);
2422 assert!(
2423 rel < 1e-12,
2424 "objective_from_apply disagrees with the dense quadratic: want={want:e} got={got:e} rel={rel:e}"
2425 );
2426 }
2427
2428 /// The structured CPU baseline must land on the SAME iterate as the dense
2429 /// oracle. Without this the honest speedup denominator could be fast for the
2430 /// wrong reason (a different, cheaper problem).
2431 #[test]
2432 fn cpu_arrow_baseline_matches_dense_reference() {
2433 let ws = small_fixture(0x2393_0C01);
2434 let opts = DeviceResidentInnerOptions::default();
2435 let dense = ws
2436 .cpu_reference_fit(&opts)
2437 .expect("dense reference fit must succeed on the PD fixture");
2438 let arrow = ws
2439 .cpu_arrow_fit(&opts)
2440 .expect("structured CPU baseline must succeed on the PD fixture");
2441 assert_eq!(arrow.execution_path, ExecutionPath::Cpu);
2442 assert_eq!(arrow.residency.operator_device_applies, 0);
2443 assert!(arrow.residency.operator_applies > 0);
2444 let scale = dense
2445 .t
2446 .iter()
2447 .chain(dense.beta.iter())
2448 .fold(1.0_f64, |m, &v| m.max(v.abs()));
2449 let mut max_rel = 0.0_f64;
2450 for (a, b) in dense.t.iter().zip(arrow.t.iter()) {
2451 max_rel = max_rel.max((a - b).abs() / scale);
2452 }
2453 for (a, b) in dense.beta.iter().zip(arrow.beta.iter()) {
2454 max_rel = max_rel.max((a - b).abs() / scale);
2455 }
2456 assert!(
2457 max_rel < 1e-9,
2458 "structured CPU baseline solves a different problem than the dense oracle: max_rel={max_rel:e}"
2459 );
2460 }
2461
2462 /// The host operator apply must be independent of the thread count: its one
2463 /// real reduction folds fixed-size row chunks in chunk order, so a 1-thread
2464 /// and an 8-thread run are bit-identical. A scheduling-dependent result here
2465 /// would move the criterion ranking across topology candidates (#1017's
2466 /// verification gate).
2467 #[test]
2468 fn host_operator_apply_is_bit_identical_across_thread_counts() {
2469 let shape = DeviceResidentArrowShape {
2470 n: 600,
2471 p: 12,
2472 basis_cols: 2,
2473 d: 2,
2474 };
2475 let ws = fixture_for_shape_seeded(shape, 0x2393_0D01)
2476 .expect("sweep-shaped fixture must validate");
2477 let t_len = shape.n * shape.d;
2478 let mut rng = SplitMix64::new(0x2393_0D02);
2479 let t: Vec<f64> = (0..t_len).map(|_| rng.sample_signed()).collect();
2480 let beta: Vec<f64> = (0..shape.p).map(|_| rng.sample_signed()).collect();
2481
2482 // Outside a rayon pool this row count takes the PARALLEL arm.
2483 let outside = ws.apply_operator_host(&t, &beta);
2484 assert!(
2485 shape.n >= OPERATOR_PARALLEL_ROW_MIN,
2486 "fixture must be large enough to take the parallel arm"
2487 );
2488 // `rayon::join` runs both closures on pool workers, so the nested-rayon
2489 // guard sends the apply down its SEQUENTIAL arm. Same chunk partition,
2490 // same fold order ⇒ the values must match EXACTLY, not approximately.
2491 let (inside, ()) = rayon::join(|| ws.apply_operator_host(&t, &beta), || ());
2492 assert_eq!(outside.cross_t, inside.cross_t);
2493 assert_eq!(outside.cross_beta, inside.cross_beta);
2494 assert_eq!(outside.border_beta, inside.border_beta);
2495 }
2496
2497 #[test]
2498 fn cpu_inner_loop_reaches_quadratic_minimiser() {
2499 let ws = small_fixture(0xABCD_0001);
2500 let opts = DeviceResidentInnerOptions::default();
2501 let outcome = ws.cpu_reference_fit(&opts).expect("cpu fit");
2502 assert!(
2503 outcome.converged,
2504 "inner loop must converge on a PD quadratic"
2505 );
2506
2507 // The stationary point satisfies H z* = g₀; verify the residual is zero.
2508 let base = ws.to_arrow_system();
2509 let (h, g0) = dense_hz(&ws, &base);
2510 let total = ws.shape.n * ws.shape.d + ws.shape.p;
2511 let mut z = Array1::<f64>::zeros(total);
2512 for r in 0..ws.shape.n * ws.shape.d {
2513 z[r] = outcome.t[r];
2514 }
2515 for c in 0..ws.shape.p {
2516 z[ws.shape.n * ws.shape.d + c] = outcome.beta[c];
2517 }
2518 let hz = h.dot(&z);
2519 let mut max_resid = 0.0_f64;
2520 for r in 0..total {
2521 max_resid = max_resid.max((hz[r] - g0[r]).abs());
2522 }
2523 assert!(
2524 max_resid < 1e-9,
2525 "inner loop fixed point must solve H z = g0; residual {max_resid:e}"
2526 );
2527 }
2528
2529 #[test]
2530 fn cpu_multiplex_matches_sequential_bit_identical() {
2531 let seeds = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66];
2532 let opts = DeviceResidentInnerOptions::default();
2533
2534 let seq_workspaces: Vec<_> = seeds.iter().map(|&s| small_fixture(s)).collect();
2535 let sequential: Vec<_> = seq_workspaces
2536 .iter()
2537 .map(|ws| ws.cpu_reference_fit(&opts).expect("seq cpu fit"))
2538 .collect();
2539
2540 let mux_workspaces: Vec<_> = seeds.iter().map(|&s| small_fixture(s)).collect();
2541 let multiplexed = run_resident_fits_multiplexed_with(mux_workspaces, opts, |ws, opts| {
2542 ws.cpu_reference_fit(opts)
2543 })
2544 .expect("multiplexed cpu fits");
2545
2546 assert_eq!(sequential.len(), multiplexed.len());
2547 for (seq, mux) in sequential.iter().zip(multiplexed.iter()) {
2548 let mux = mux.as_ref().expect("mux fit ok");
2549 // Independent fits: scheduling cannot change the numbers, so the
2550 // parallel result must be bit-for-bit identical to sequential.
2551 assert_eq!(seq.t.as_slice(), mux.outcome.t.as_slice());
2552 assert_eq!(seq.beta.as_slice(), mux.outcome.beta.as_slice());
2553 assert_eq!(seq.objective.to_bits(), mux.outcome.objective.to_bits());
2554 }
2555 }
2556
2557 /// #1017 Phase 3 residency parity. On a CUDA host the device-resident inner
2558 /// loop (`device_fit`, which keeps the Hessian factors on-device across
2559 /// iterations via `ResidentArrowFrameHandle`) must reach the same minimiser
2560 /// as the fully independent CPU dense-reference loop (`cpu_reference_fit`,
2561 /// which re-factors per iterate). On a CPU-only host the resident path must
2562 /// decline cleanly (`Unavailable`) rather than silently disagree, and the
2563 /// resident-frame handle construction must likewise decline — so the gate is
2564 /// meaningful on the build box and the wall-clock arm runs on the GPU node.
2565 #[test]
2566 fn device_resident_fit_matches_cpu_reference() {
2567 let ws = small_fixture(0x5AE_1017);
2568 let opts = DeviceResidentInnerOptions::default();
2569
2570 // CPU reference (re-factors per iterate) — always available.
2571 let cpu = ws.cpu_reference_fit(&opts).expect("cpu reference fit");
2572 assert!(cpu.converged, "cpu reference must converge on PD quadratic");
2573
2574 let base = ws.to_arrow_system();
2575
2576 println!(
2577 "DIAG_RESIDENT device_resident={} shape=({},{},{})",
2578 ws.device_resident(),
2579 ws.shape.n,
2580 ws.shape.d,
2581 ws.shape.p
2582 );
2583 if ws.device_resident() {
2584 // Resident device loop: factors stay on-device across iterations.
2585 let dev = ws.device_fit(&opts).expect("device resident fit");
2586 assert_eq!(
2587 dev.execution_path,
2588 ExecutionPath::GpuResidentFull,
2589 "device_fit must report the full device-resident execution path"
2590 );
2591 assert!(dev.converged, "device resident loop must converge");
2592
2593 // Certified-refinement parity (#1014): the resident path and the
2594 // independent CPU path solve the same quadratic, so their minimisers
2595 // agree to a tight relative tolerance. The resident path differs from
2596 // the reference only by SKIPPING re-derivation of g-independent
2597 // factor work, not by changing the arithmetic.
2598 let t_scale = cpu.t.iter().fold(1.0_f64, |m, &v| m.max(v.abs()));
2599 let b_scale = cpu.beta.iter().fold(1.0_f64, |m, &v| m.max(v.abs()));
2600 let mut max_rel = 0.0_f64;
2601 for (a, b) in dev.t.iter().zip(cpu.t.iter()) {
2602 max_rel = max_rel.max((a - b).abs() / t_scale);
2603 }
2604 for (a, b) in dev.beta.iter().zip(cpu.beta.iter()) {
2605 max_rel = max_rel.max((a - b).abs() / b_scale);
2606 }
2607 assert!(
2608 max_rel < 1e-9,
2609 "resident device fit must match CPU reference (rel {max_rel:e})"
2610 );
2611
2612 // The public single-iteration API must use the same resident-frame
2613 // mechanism as device_fit (constant factors held resident, only the
2614 // gradient uploaded), not the re-uploading arrow-Schur entry. Because
2615 // it is a SINGLE solve at one frozen frame, the truthful path is
2616 // `GpuResidentLinearization`, not the full inner-loop `GpuResidentFull`.
2617 let one = ws
2618 .one_inner_iteration(opts.initial_ridge_t, opts.initial_ridge_beta)
2619 .expect("resident one_inner_iteration");
2620 assert_eq!(
2621 one.execution_path,
2622 ExecutionPath::GpuResidentLinearization,
2623 "one_inner_iteration must report resident single-linearization residency"
2624 );
2625
2626 // The resident frame's single-gradient solve must also match a full
2627 // independent solve at the same gradient (the per-iterate contract).
2628 // `ResidentArrowFrameHandle` is UNINHABITED on CPU-only hosts, so a
2629 // `let … .expect()` binding marks everything after it unreachable
2630 // under `-D warnings`; the consuming assertions therefore live
2631 // inside the `Ok` arm (dead match arms are lint-exempt), exactly
2632 // like the production consumers.
2633 match crate::gpu_kernels::arrow_schur::ResidentArrowFrameHandle::new(
2634 &base,
2635 opts.initial_ridge_t,
2636 opts.initial_ridge_beta,
2637 None,
2638 ) {
2639 Err(err) => panic!("resident frame must build on CUDA host: {err:?}"),
2640 Ok(frame) => {
2641 let g_t: Vec<f64> = base
2642 .rows
2643 .iter()
2644 .flat_map(|r| r.gt.iter().copied())
2645 .collect();
2646 let g_beta: Vec<f64> = base.gb.iter().copied().collect();
2647 let resident_sol = frame
2648 .solve_gradient(&g_t, &g_beta)
2649 .expect("resident single-gradient solve");
2650 let full =
2651 crate::gpu_kernels::arrow_schur::solve_arrow_newton_step_dense_reference(
2652 &base,
2653 opts.initial_ridge_t,
2654 opts.initial_ridge_beta,
2655 )
2656 .expect("dense reference single solve");
2657 let mut max_step_rel = 0.0_f64;
2658 let step_scale = full
2659 .delta_t
2660 .iter()
2661 .chain(full.delta_beta.iter())
2662 .fold(1.0_f64, |m, &v| m.max(v.abs()));
2663 for (a, b) in resident_sol.delta_t.iter().zip(full.delta_t.iter()) {
2664 max_step_rel = max_step_rel.max((a - b).abs() / step_scale);
2665 }
2666 for (a, b) in resident_sol.delta_beta.iter().zip(full.delta_beta.iter()) {
2667 max_step_rel = max_step_rel.max((a - b).abs() / step_scale);
2668 }
2669 assert!(
2670 max_step_rel < 1e-9,
2671 "resident solve_gradient must match full dense reference step \
2672 (rel {max_step_rel:e})"
2673 );
2674 }
2675 }
2676
2677 // The re-uploading GPU loop (residency baseline) must reach the same
2678 // minimiser as both the resident loop and the CPU reference.
2679 let reup = ws
2680 .device_reupload_fit(&opts)
2681 .expect("device re-uploading fit");
2682 assert_eq!(
2683 reup.execution_path,
2684 ExecutionPath::GpuReupload,
2685 "device_reupload_fit must report the re-uploading device path"
2686 );
2687 assert!(reup.converged, "re-uploading loop must converge");
2688 let mut max_reup_rel = 0.0_f64;
2689 for (a, b) in reup.t.iter().zip(cpu.t.iter()) {
2690 max_reup_rel = max_reup_rel.max((a - b).abs() / t_scale);
2691 }
2692 for (a, b) in reup.beta.iter().zip(cpu.beta.iter()) {
2693 max_reup_rel = max_reup_rel.max((a - b).abs() / b_scale);
2694 }
2695 assert!(
2696 max_reup_rel < 1e-9,
2697 "re-uploading GPU fit must match CPU reference (rel {max_reup_rel:e})"
2698 );
2699 } else {
2700 // The fixture is sized (batch = 8) to clear the device dispatch floor,
2701 // so on a host WITH a CUDA runtime `device_resident()` must be true and
2702 // we take the device branch above. Reaching this branch with a runtime
2703 // present means the device binding silently failed — which would mask a
2704 // real upload/dispatch fault behind the CPU-decline path (the
2705 // device-PCG skip-pass class, eee12f6b2). Fail loud unless this is a
2706 // genuinely CPU-only host.
2707 assert!(
2708 gam_gpu::device_runtime::GpuRuntime::resolve(gam_gpu::GpuPolicy::Auto)
2709 .unwrap_or_else(|error| {
2710 panic!("GPU probe fault in resident SAE engagement test: {error}")
2711 })
2712 .is_none(),
2713 "device_resident() is false on a host WITH a CUDA runtime present, \
2714 despite a floor-clearing fixture (batch=8): the resident device \
2715 buffers failed to bind — a real device fault, not a CPU-only skip."
2716 );
2717 // CPU-only host: the resident path must decline, not disagree.
2718 let dev = ws.device_fit(&opts);
2719 assert!(
2720 matches!(dev, Err(DeviceResidentArrowError::Unavailable { .. })),
2721 "device_fit must report Unavailable on a CPU-only host, got {dev:?}"
2722 );
2723 let reup = ws.device_reupload_fit(&opts);
2724 assert!(
2725 matches!(reup, Err(DeviceResidentArrowError::Unavailable { .. })),
2726 "device_reupload_fit must report Unavailable on a CPU-only host, got {reup:?}"
2727 );
2728 let frame = crate::gpu_kernels::arrow_schur::ResidentArrowFrameHandle::new(
2729 &base,
2730 opts.initial_ridge_t,
2731 opts.initial_ridge_beta,
2732 None,
2733 );
2734 assert!(
2735 frame.is_err(),
2736 "resident frame construction must decline on a CPU-only host"
2737 );
2738 }
2739 }
2740
2741 /// #1017 fit-path parity (CPU-runnable). The resident inner solve and the
2742 /// PRODUCTION arrow-Schur inner solve (`solve_arrow_newton_step_core`, the
2743 /// entry the SAE joint fit reaches through `solve_with_lm_escalation_inner`)
2744 /// must solve the SAME bordered-quadratic Newton system.
2745 ///
2746 /// This is the cross-implementation parity behind wiring the device seam into
2747 /// the SAE inner loop: `solve_arrow_newton_step_core` carries the #1017
2748 /// device-Schur seam (and falls through bit-identically to its CPU path off
2749 /// CUDA), and the resident workspace's `cpu_reference_fit` converges the same
2750 /// quadratic `φ(z) = ½‖X‖² + ½ zᵀH z − g₀ᵀ z`. The resident converged iterate
2751 /// `z*` is the stationary point `H z* = g₀`; the production arrow path solves
2752 /// the Newton system `H Δ = −g₀` from `z = 0`, so its step is
2753 /// `Δ = −H⁻¹ g₀ = −z*`. With `H` PD the exact relationship is therefore
2754 /// `Δ = −z*`; asserting it pins that routing the production inner solve through
2755 /// the device-aware `_core` (which a GPU host then offloads) solves the
2756 /// identical system the resident loop does. Runs on the CPU build box — no
2757 /// CUDA required.
2758 #[test]
2759 fn resident_inner_solve_matches_production_arrow_core() {
2760 use crate::arrow_schur::{ArrowSolveOptions, solve_arrow_newton_step_core};
2761
2762 let ws = small_fixture(0x1017_F17);
2763 let opts = DeviceResidentInnerOptions::default();
2764
2765 // Resident workspace converged fit (re-factoring CPU reference loop).
2766 let resident = ws.cpu_reference_fit(&opts).expect("resident cpu fit");
2767 assert!(
2768 resident.converged,
2769 "resident reference must converge on the PD quadratic"
2770 );
2771
2772 // Production arrow path: one Newton step on the same system from z = 0.
2773 // `_core` is the device-aware entry; on this CPU box it runs the dense
2774 // CPU solve, the exact path the GPU host would fall back to on decline.
2775 let sys = ws.to_arrow_system();
2776 let (delta_t, delta_beta, _diag) = solve_arrow_newton_step_core(
2777 &sys,
2778 opts.initial_ridge_t,
2779 opts.initial_ridge_beta,
2780 &ArrowSolveOptions::direct(),
2781 )
2782 .expect("production arrow-core solve");
2783
2784 // The Newton step from z = 0 is Δ = −H⁻¹g₀ = −z*, where z* is the resident
2785 // converged iterate (H z* = g₀, the invariant
2786 // `cpu_inner_loop_reaches_quadratic_minimiser` pins directly). With H PD
2787 // the relationship is exact, so Δ + z* = 0 to factorisation tolerance.
2788 let t_scale = resident.t.iter().fold(1.0_f64, |m, &v| m.max(v.abs()));
2789 let b_scale = resident.beta.iter().fold(1.0_f64, |m, &v| m.max(v.abs()));
2790 // #1399: report the t-block and beta-block mismatch SEPARATELY (not one
2791 // fused scalar). The two halves localise a divergence: a t-block-only gap
2792 // points at the per-row factor / row gradient assembly, a beta-block gap
2793 // at the border Schur path — turning the opaque overall rel into an
2794 // actionable signal for the resident-vs-production parity divergence.
2795 let mut max_rel_t = 0.0_f64;
2796 let mut worst_t: Option<(usize, f64, f64)> = None;
2797 for (i, (prod, res)) in delta_t.iter().zip(resident.t.iter()).enumerate() {
2798 let rel = (prod + res).abs() / t_scale;
2799 if rel > max_rel_t {
2800 max_rel_t = rel;
2801 worst_t = Some((i, *prod, *res));
2802 }
2803 }
2804 let mut max_rel_b = 0.0_f64;
2805 let mut worst_b: Option<(usize, f64, f64)> = None;
2806 for (i, (prod, res)) in delta_beta.iter().zip(resident.beta.iter()).enumerate() {
2807 let rel = (prod + res).abs() / b_scale;
2808 if rel > max_rel_b {
2809 max_rel_b = rel;
2810 worst_b = Some((i, *prod, *res));
2811 }
2812 }
2813 let max_rel = max_rel_t.max(max_rel_b);
2814 assert!(
2815 max_rel < 1e-9,
2816 "production arrow-core Newton step must be −(resident converged fit) on \
2817 the same quadratic; wiring the device seam into the SAE inner loop must \
2818 not change the system being solved. rel_t={max_rel_t:e} (worst {worst_t:?}: \
2819 Δt+t* must be 0), rel_beta={max_rel_b:e} (worst {worst_b:?}: Δβ+β* must \
2820 be 0). A t-only gap implicates the per-row factor / row-gradient \
2821 assembly; a β-only gap the border Schur path."
2822 );
2823 }
2824
2825 /// #1017 residency-isolating per-solve bench. A full-fit wall-clock bench
2826 /// runs an exact quadratic that converges
2827 /// in ONE Newton step, so the resident frame is built once and solved once —
2828 /// the across-iteration amortization (factor `D`/`B`/Schur once, reuse for
2829 /// every gradient) has nothing to amortize over and the measured speedup is
2830 /// only the single-solve `D`/`B` upload saving.
2831 ///
2832 /// This bench isolates the residency lever the way the production inner loop
2833 /// actually exercises it: at a frozen gate/basis frame the Hessian blocks are
2834 /// CONSTANT and the SAE inner Newton takes MANY gradient solves against them.
2835 /// It therefore times
2836 /// * RESIDENT: build the [`crate::gpu_kernels::arrow_schur::ResidentArrowFrameHandle`]
2837 /// ONCE, then `N` `solve_gradient` calls (upload only the `O(n·d + k)`
2838 /// gradient per solve; no POTRF, no `D`/`B` re-upload);
2839 /// * REUPLOAD: `N` `solve_arrow_newton_step` calls (re-pack/upload `D`/`B`/`g`
2840 /// and re-run the per-row POTRF + border Schur factor every call).
2841 /// Both produce bit-identical steps; the ratio is the pure across-iteration
2842 /// residency speedup, which is what #1017 Phase 3 buys per inner iteration.
2843 /// `N` mirrors a realistic SAE inner-Newton iteration count. CPU-only hosts
2844 /// print a skip line. Run with `--nocapture`.
2845 #[test]
2846 fn gpu_residency_per_solve_bench() {
2847 use std::time::Instant;
2848 const N_SOLVES: usize = 24;
2849 for (label, ws) in [
2850 ("color_arm", super::color_arm_fixture()),
2851 ("qwen_non_gating", super::qwen_non_gating_fixture()),
2852 ] {
2853 let ws = ws.expect("bench fixture must validate");
2854 let base = ws.to_arrow_system();
2855 // A family of distinct gradients standing in for the per-iterate
2856 // residual r(z) = H z − g₀ the inner loop feeds. Distinct gradients
2857 // make the reupload path redo the (g-independent) factor work each
2858 // time — exactly the waste residency removes.
2859 let n = ws.shape.n;
2860 let d = ws.shape.d;
2861 let p = ws.shape.p;
2862 let gradients: Vec<(Vec<f64>, Vec<f64>)> = (0..N_SOLVES)
2863 .map(|s| {
2864 let g_t: Vec<f64> =
2865 (0..n * d).map(|i| ((i + s) as f64 * 0.001).sin()).collect();
2866 let g_beta: Vec<f64> = (0..p)
2867 .map(|j| ((j + 7 * s) as f64 * 0.0007).cos())
2868 .collect();
2869 (g_t, g_beta)
2870 })
2871 .collect();
2872
2873 if !ws.device_resident() {
2874 println!(
2875 "[#1017 per-solve {label}] no CUDA device — {N_SOLVES} solves skipped; \
2876 run on the GPU node for the across-iteration residency speedup"
2877 );
2878 continue;
2879 }
2880
2881 // Build the resident frame ONCE (its factor cost is the across-
2882 // iteration amortization the bench is measuring, so it is timed
2883 // separately from the per-solve loop).
2884 let t_build = Instant::now();
2885 // `ResidentArrowFrameHandle` is UNINHABITED on CPU-only hosts: a
2886 // `let … .expect()` binding marks everything after it unreachable
2887 // under `-D warnings`, so the bench body consumes the frame inside
2888 // the `Ok` arm — the same pattern as the production consumers.
2889 match crate::gpu_kernels::arrow_schur::ResidentArrowFrameHandle::new(
2890 &base, 0.0, 0.0, None,
2891 ) {
2892 Err(err) => panic!("resident frame must build on CUDA host: {err:?}"),
2893 Ok(frame) => {
2894 let frame_build_ms = t_build.elapsed().as_secs_f64() * 1e3;
2895
2896 // Warm-up: one solve on each path before timing so the residency
2897 // ratio reflects steady-state per-iterate cost, not the one-time
2898 // NVRTC/cuSOLVER handle init, module JIT, or first-touch device
2899 // allocation (those are paid once per process, not per inner
2900 // iteration). The production inner loop pays them once and then runs
2901 // MANY solves, which is exactly the regime this assertion guards.
2902 frame
2903 .solve_gradient(&gradients[0].0, &gradients[0].1)
2904 .expect("resident warm-up solve");
2905 {
2906 let mut sys = ws.to_arrow_system();
2907 for (i, row) in sys.rows.iter_mut().enumerate() {
2908 for r in 0..d {
2909 row.gt[r] = gradients[0].0[i * d + r];
2910 }
2911 }
2912 for (j, gb) in sys.gb.iter_mut().enumerate() {
2913 *gb = gradients[0].1[j];
2914 }
2915 sys.refresh_row_hessian_fingerprint();
2916 crate::gpu_kernels::arrow_schur::solve_arrow_newton_step(
2917 &sys, 0.0, 0.0, None,
2918 )
2919 .expect("reupload warm-up solve");
2920 }
2921
2922 // RESIDENT: reuse the (already-built, already-warmed) frame for N
2923 // gradient-only solves. Times ONLY the per-iterate gradient solves —
2924 // upload `O(n·d + k)` gradient, run the cheap residual path, read
2925 // back `δ`. No POTRF, no `D`/`B` re-upload.
2926 let t_res = Instant::now();
2927 let mut resident_steps = Vec::with_capacity(N_SOLVES);
2928 for (g_t, g_beta) in &gradients {
2929 resident_steps.push(
2930 frame
2931 .solve_gradient(g_t, g_beta)
2932 .expect("resident solve_gradient"),
2933 );
2934 }
2935 let resident_ms = t_res.elapsed().as_secs_f64() * 1e3;
2936
2937 // REUPLOAD: N full solves, each re-uploading D/B/g and re-factoring.
2938 let t_reup = Instant::now();
2939 let mut reupload_steps = Vec::with_capacity(N_SOLVES);
2940 for (g_t, g_beta) in &gradients {
2941 let mut sys = ws.to_arrow_system();
2942 for (i, row) in sys.rows.iter_mut().enumerate() {
2943 for r in 0..d {
2944 row.gt[r] = g_t[i * d + r];
2945 }
2946 }
2947 for (j, gb) in sys.gb.iter_mut().enumerate() {
2948 *gb = g_beta[j];
2949 }
2950 sys.refresh_row_hessian_fingerprint();
2951 reupload_steps.push(
2952 crate::gpu_kernels::arrow_schur::solve_arrow_newton_step(
2953 &sys, 0.0, 0.0, None,
2954 )
2955 .expect("reupload solve_arrow_newton_step"),
2956 );
2957 }
2958 let reupload_ms = t_reup.elapsed().as_secs_f64() * 1e3;
2959
2960 // Parity: resident and reupload steps must be bit-identical (same
2961 // factor kernels; residency only skips re-deriving g-independent work).
2962 let mut max_rel = 0.0_f64;
2963 for (rs, us) in resident_steps.iter().zip(reupload_steps.iter()) {
2964 let scale = us
2965 .delta_t
2966 .iter()
2967 .chain(us.delta_beta.iter())
2968 .fold(1.0_f64, |m, &v| m.max(v.abs()));
2969 for (a, b) in rs.delta_t.iter().zip(us.delta_t.iter()) {
2970 max_rel = max_rel.max((a - b).abs() / scale);
2971 }
2972 for (a, b) in rs.delta_beta.iter().zip(us.delta_beta.iter()) {
2973 max_rel = max_rel.max((a - b).abs() / scale);
2974 }
2975 }
2976
2977 let resident_per_solve = resident_ms / N_SOLVES as f64;
2978 let reupload_per_solve = reupload_ms / N_SOLVES as f64;
2979 let residency_speedup = reupload_ms / resident_ms.max(1e-9);
2980 println!(
2981 "[#1017 per-solve {label}] N={N_SOLVES} frame_build={frame_build_ms:.2}ms \
2982 resident={resident_ms:.2}ms ({resident_per_solve:.3}ms/solve, \
2983 grad-upload + warm factors) reupload={reupload_ms:.2}ms \
2984 ({reupload_per_solve:.3}ms/solve, N factors + N D/B uploads) \
2985 residency_speedup={residency_speedup:.2}x parity_rel={max_rel:e}"
2986 );
2987 assert!(
2988 max_rel < 1e-9,
2989 "{label}: resident per-solve steps must match reupload (rel {max_rel:e})"
2990 );
2991
2992 // #1017 deliverable 2: the residency amortization must actually fire
2993 // on hardware — reusing the resident factors across iterations has to
2994 // be STRICTLY cheaper per solve than re-uploading D/B/g and
2995 // re-factoring every iterate. This is the core perf claim, asserted
2996 // (not merely printed) so a regression that silently re-uploads, or a
2997 // dispatch change that drops the resident path, fails the gate on the
2998 // A100 instead of slipping through as a slower-but-green run.
2999 //
3000 // The `color_arm` shape (n=180, p=5120) is the decisive case: the
3001 // per-solve reupload pays a 5120-wide border Schur factor + the
3002 // `O(n·d·p)` cross-block upload every iterate, while the resident path
3003 // pays only the `O(n·d + p)` gradient transfer and two border TRSMs.
3004 // We require a clear >1.5x margin there. The `qwen_non_gating` shape
3005 // (p=2048) has a smaller border so its margin is thinner; we still
3006 // require a genuine speedup (>1x) but do not over-tighten it.
3007 let min_speedup = if label == "color_arm" { 1.5 } else { 1.0 };
3008 assert!(
3009 residency_speedup > min_speedup,
3010 "{label}: across-iteration residency must beat per-solve re-upload \
3011 (residency_speedup={residency_speedup:.3}x, required >{min_speedup}x; \
3012 resident {resident_per_solve:.3}ms/solve vs reupload \
3013 {reupload_per_solve:.3}ms/solve over N={N_SOLVES} solves) — the resident \
3014 frame either silently re-uploaded D/B or the dispatch dropped the \
3015 amortized factor path"
3016 );
3017 }
3018 }
3019 }
3020 }
3021
3022 /// #1017 Phase 4 variant sweep: an OLMo-battery-shaped matrix of independent
3023 /// fits (here K{1..4} × 3 basis widths = 12 color-arm variants) dispatched
3024 /// concurrently on one device. This is the cross-fit throughput lever — the
3025 /// fits are independent, so multiplexing changes only scheduling.
3026 fn battery_variant_matrix() -> Vec<super::SweepVariant> {
3027 let mut variants = Vec::new();
3028 // K is the topology rank; the battery races K{1..4}. Each K × basis cell
3029 // is an independent fit. Color-arm border, varied basis_cols per cell.
3030 for k in 1..=4u64 {
3031 for basis_cols in [4usize, 8, 12] {
3032 let mut dim = DeviceResidentArrowShape::color_arm();
3033 dim.basis_cols = basis_cols;
3034 variants.push(super::SweepVariant {
3035 dim,
3036 seed: 0x1017_0040_0000_0000 ^ (k << 8) ^ (basis_cols as u64),
3037 });
3038 }
3039 }
3040 variants
3041 }
3042
3043 /// Same K{1..4} × 3-basis battery matrix as [`battery_variant_matrix`], on
3044 /// a small border. The multiplex parity property is SCHEDULING invariance
3045 /// — it is independent of `p` — while the CPU reference oracle factors the
3046 /// full dense joint Hessian with a naive scalar Cholesky, O((n·d + p)³)
3047 /// per fit. At the color-arm border (p = 5120) that oracle alone cost the
3048 /// suite 67 minutes at 0% GPU (#1017 datum, 2026-07-20); at p = 256 the
3049 /// identical property costs sub-second. The wide border stays exercised on
3050 /// the device by [`gpu_multiplex_throughput_bench`], which keeps the full
3051 /// color-arm matrix.
3052 fn battery_variant_matrix_cpu_gate() -> Vec<super::SweepVariant> {
3053 battery_variant_matrix()
3054 .into_iter()
3055 .map(|mut variant| {
3056 variant.dim.p = 256;
3057 variant
3058 })
3059 .collect()
3060 }
3061
3062 /// Phase-4 parity: the multiplexed sweep must be bit-identical to running the
3063 /// same fits sequentially (CPU reference path here so the gate runs on the
3064 /// build box; the device path is exercised by the throughput bench on the a100).
3065 #[test]
3066 fn variant_sweep_multiplex_matches_sequential() {
3067 let variants = battery_variant_matrix_cpu_gate();
3068 let opts = DeviceResidentInnerOptions::default();
3069
3070 // Multiplexed via the CPU-reference runner so the gate is meaningful
3071 // without CUDA, exercising the exact run_topology_race_parallel plumbing.
3072 let workspaces =
3073 super::build_sweep_workspaces(&variants).expect("sweep workspaces must build");
3074 let multiplexed =
3075 super::run_resident_fits_multiplexed_with(workspaces, opts, |ws, opts| {
3076 ws.cpu_reference_fit(opts)
3077 })
3078 .expect("multiplexed cpu sweep");
3079
3080 let seq_workspaces =
3081 super::build_sweep_workspaces(&variants).expect("sweep workspaces must build");
3082 let sequential: Vec<_> = seq_workspaces
3083 .iter()
3084 .map(|ws| ws.cpu_reference_fit(&opts))
3085 .collect();
3086
3087 assert_eq!(multiplexed.len(), sequential.len());
3088 for (idx, (mux, seq)) in multiplexed.iter().zip(sequential.iter()).enumerate() {
3089 let mux = &mux.as_ref().unwrap().outcome;
3090 let seq = seq.as_ref().unwrap();
3091 assert_eq!(
3092 mux.t.as_slice(),
3093 seq.t.as_slice(),
3094 "variant {idx}: multiplexed t differs from sequential"
3095 );
3096 assert_eq!(
3097 mux.beta.as_slice(),
3098 seq.beta.as_slice(),
3099 "variant {idx}: multiplexed beta differs from sequential"
3100 );
3101 assert_eq!(
3102 mux.objective.to_bits(),
3103 seq.objective.to_bits(),
3104 "variant {idx}: multiplexed objective differs from sequential"
3105 );
3106 }
3107 }
3108
3109}