Linux CUDA GPU opportunities for R-MSRM, GP-MAP and GP posterior¶
Measurement date: September 21, 2026. Status: research note on the development source at Git HEAD 5a49a54; no package release, default change, or production optimization is made by this note.
This note builds on the prior Apple MPS findings and the post-release Linux evaluation plan. It answers three questions with measurements on the user's Linux workstation: where GPU acceleration could help each model, how much float64 throughput the installed NVIDIA cards actually provide, and which bottlenecks are algorithmic rather than hardware limits.
Summary¶
- R-MSRM does not need a GPU. A bounded fit at the recorded one-eighth empirical scale spends its time in many small per-block NumPy/SciPy operations and Python-level loops. The largest single operation, the dense reconstruction residual, is about 30 ms. Restarts already run as parallel processes across the 64 CPU cores. Keep R on CPU.
- GP-MSRM already runs on CUDA with no code change. The grouped and dense likelihoods are JAX; the guarded symmetric factorization selects the CPU LAPACK callback only on the CPU platform and native Cholesky elsewhere. Installing
jax[cuda13]and starting Python with the CUDA platform is sufficient to run the released code on a GPU in float64. GPU support in the package is therefore an installation, configuration, evidence-recording and testing task, not a port. - Float64 throughput on these cards is limited by design, and the PRO 6000 is the useful one. Both the RTX 3090 (Ampere, compute capability 8.6) and the RTX PRO 6000 Blackwell (compute capability 12.0) execute float64 at 1/64 of their float32 rate. Measured float64 matrix multiplication peaks near 0.55 TFLOPS on the 3090 and 2.1 TFLOPS on the PRO 6000, against roughly 1 TFLOPS from the 64-core CPU under the day's background load. For the model's actual objective-and-gradient at the one-eighth scale the PRO 6000 took 66 ms, the 3090 172 ms and the best CPU row 644 ms. Float32 is 30 to 80 times faster on the GPUs (and JAX's default float32 matmul on CUDA silently uses TF32) but is outside the float64 numerical contract, and the MPS note already recorded a float32 factorization failing gradient qualification.
- The current CPU GP path had a large avoidable cost in this environment. The CPU LAPACK callback took 0.4 to 2.9 s for a 2,727-dimension factorization depending on OpenBLAS thread count and background load, while direct POTRF+POTRI with 16 threads took 60 ms and the 3090 took 135 ms. Thread control is the first fix for the CPU path; it needs no GPU.
- For full recordings the dense grouped algebra is the wall, not the device. Cost grows with the cube of unique observation nodes times factors. A single run eight times longer than the one-eighth case at K=5 implies a 36,000-dimension dense factorization per gradient, which is tens of seconds on any device here and tens of gigabytes of memory. If the full data are eight separate runs, cost grows only linearly and runs are independent. The state-space formulation, with grouped observation updates and a parallel-in-time scan, is the candidate that changes the scaling; the plan already names it.
- The posterior is where a GPU pays, and it is still expensive. One NUTS leapfrog step for the K = 3 one-eighth case cost 2.0 s on the CPU, 0.37 s on the 3090 and 0.16 s on the PRO 6000, in each case two to three times the bare gradient. Vectorizing two chains on one card gained about 1.35x throughput, not 2x. At realistic budgets (a thousand warmup and twelve hundred draws at a few hundred steps each) one chain at this scale is on the order of a day on the 3090 and half a day on the PRO 6000, versus about a week on the CPU. The ArviZ diagnostics phase took 70 to 100 s for only 15 draws over 57,757 parameters on every device and grows with draw count; it needs its own profile.
- The state-space backend cannot be measured at empirical scale as written. Two Gaussian responses give 50 states per factor, so the state dimension is 150 at K = 3. The implementation stages one transition and one process-noise matrix per scalar observation, 17.6 GB for a 48,000-observation run, and reverse-mode differentiation through the scan stores per-step covariances on top: the 96 GB card requested a single 214 GB allocation for the gradient on a 36-feature reduction, and the 3090 failed earlier. At 3.6 million observations the same arrays would be terabytes. Grouping observations by unique node (909 here) reduces the transition arrays to 327 MB and the scan length by a factor of 50. Grouped observation updates, with gradient checkpointing across the scan, are therefore a prerequisite, not an optimization, for state-space at empirical scale on any device.
The rest of this note records the evidence and its limits.
Hardware and runtime inventory¶
| Item | Value |
|---|---|
| CPU | AMD Ryzen Threadripper PRO 3995WX, 64 cores / 128 threads, one NUMA node |
| Host memory | 503 GB |
| GPU 0 | NVIDIA RTX PRO 6000 Blackwell Workstation Edition, 96 GB (97,887 MiB), compute capability 12.0, PCIe 4.0 x16 |
| GPU 1 | NVIDIA GeForce RTX 3090, 24 GB (24,576 MiB), compute capability 8.6, PCIe 4.0 x16 |
| Driver / CUDA | 580.173.02 / CUDA 13.0 |
| Default Python | Anaconda 3.11.7 with NumPy 2.4.6 and torch 2.3.1 (CUDA 12.1, no sm_120 kernels, so torch cannot target the Blackwell card); no JAX; below the package's Python 3.12 floor |
| Measurement environment | uv venv --python 3.12, package installed from the checkout with the bayesian and test extras, jax==0.11.2, jaxlib==0.11.2, jax[cuda13] plugin, numpyro==0.22.0, NumPy 2.5.3, SciPy 1.18.1 with OpenBLAS 0.3.34 |
Both GPUs were in use by another lab member's jobs during the first part of the session (about 10 GB resident on the 3090, 5 to 8 GB on the PRO 6000, intermittent utilization), and the CPU load average was between 25 and 65 from other users' work throughout. All timings below are therefore upper bounds on contended hardware, recorded with XLA_PYTHON_CLIENT_PREALLOCATE=false. At the user's request, measurement first moved to the 3090 only; the full PRO 6000 series ran later once that card showed zero utilization and under 1 GB resident, with the user's authorization. The CPU rows remain the least reliable and need an idle-machine repeat.
The MPS note's lessons were followed: one frozen source tree, separate preparation, compilation and warm timings, device synchronization before stopping timers, callback and fallback behavior observed, and float64 throughout.
Raw float64 linear algebra¶
bench_linalg.py times a square matrix product, Cholesky, LU and Cholesky-inverse (cho_solve against the identity, the pattern used by the native symmetric solve) with JAX on each device. Median of three warm repeats after one compile.
| Device | dtype | n | matmul | TFLOPS | Cholesky | LU | Cholesky inverse |
|---|---|---|---|---|---|---|---|
| CPU (XLA, all cores) | float64 | 2,700 | 41 ms | 0.95 | 57 ms | 140 ms | 115 ms |
| CPU | float64 | 4,096 | 119 ms | 1.16 | 141 ms | 442 ms | 465 ms |
| CPU | float64 | 8,192 | 1,958 ms | 0.56 | 3,086 ms | 9,064 ms | 4,690 ms |
| RTX 3090 | float64 | 2,700 | 77 ms | 0.51 | 20 ms | 51 ms | 141 ms |
| RTX 3090 | float64 | 4,096 | 246 ms | 0.56 | 55 ms | 130 ms | 417 ms |
| RTX 3090 | float64 | 8,192 | 2,032 ms | 0.54 | 432 ms | 894 ms | 3,094 ms |
| RTX PRO 6000 | float64 | 2,700 | 20 ms | 1.92 | 8.2 ms | 22 ms | 41 ms |
| RTX PRO 6000 | float64 | 4,096 | 66 ms | 2.10 | 16 ms | 46 ms | 116 ms |
| RTX PRO 6000 | float64 | 8,192 | 583 ms | 1.88 | 119 ms | 277 ms | 872 ms |
| RTX PRO 6000 | float64 | 16,384 | 4,677 ms | 1.88 | 807 ms | 1,702 ms | 6,608 ms |
| RTX 3090 | float32 | 2,700 | 1.2 ms | 32 | 1.4 ms | 8 ms | 7 ms |
| RTX 3090 | float32 | 8,192 | 30 ms | 36 | 22 ms | 50 ms | 105 ms |
| RTX PRO 6000 | float32 | 8,192 | 6.7 ms | 165 | 6.3 ms | 24 ms | 36 ms |
| RTX PRO 6000 | float32 | 16,384 | 73 ms | 121 | 35 ms | 102 ms | 253 ms |
Readings:
- In float64 the 3090 is at rough parity with the loaded CPU for the factorization sizes the grouped model produces today, and slower for matrix products. The PRO 6000 is about 3 to 4 times faster than the CPU in float64 at these sizes, sustains its rate to 16,384 dimensions, and is 3.6x the 3090 at 8,192. Neither approaches the float32 numbers, which are excluded by the numerical contract.
- The PRO 6000 float32 matrix product exceeds the card's nominal float32 rate, which shows that JAX's default float32 matmul precision on CUDA uses TF32 tensor cores. Any float32 experiment must set
jax_default_matmul_precision="highest"or it measures a reduced-precision path. - The CPU rows at n = 8,192 are anomalous (matmul throughput halves) and reflect the background load, not a hardware limit.
- Memory, not time, bounds single-run scaling on the 3090: a 36,000-dimension float64 matrix is 10.4 GB and the inverse path holds several. The PRO 6000's 96 GB removes that bound for a single run, at about 70 s per factorization-plus-inverse by cubic extrapolation from 16,384.
R-MSRM: CPU profile at the recorded scale¶
Synthetic workload shaped like the recorded one-eighth empirical case: four participants with a 3,600-feature brain stream sampled every 1.8 s (252 samples), five participants with rating and physiology streams on 1.2 s and 1.5 s clocks, one run of about 454 s, K = 3, latent_dt = 0.9 (505 grid points), Identity brain response, Gaussian rating lag and Gaussian physiology width and lag estimated with shared pooling. profile_r.py ran one restart for three alternating iterations with 8 OpenBLAS threads under cProfile.
| Quantity | Value |
|---|---|
| Total fit time (3 iterations) | 2.65 s, about 0.7 s per alternating iteration after 0.5 s of preprocessing |
complete_objective |
35 calls, 1.06 s total, 30 ms each: dense residual over 3.6 million observed values |
| Gaussian operator construction with derivatives | 250 calls, 0.58 s total, 2.3 ms each |
make_blocks + fit_preprocessing |
0.46 s once |
solve_latents (sparse normal equations, SuperLU) |
4 calls, 0.19 s total, 21 ms in the solve itself |
| Loading solves, gradient terms, scale balance | under 0.2 s combined |
A repeat at twice the recording length (1,009 grid points, 7.3 million observed values, load average 47 to 58) took 6.6 s for the same three iterations: 1.7 s of one-time block preparation, then about 1.6 s per iteration with the reconstruction residual at 40 ms per call and the Gaussian operator construction at 5 ms per call. Growth is close to linear in recording length once the background load is discounted.
Extrapolation: a 100-iteration restart costs on the order of one to two minutes at this scale, and the per-iteration cost scales roughly linearly with recording length because every term is per-observation-row or banded. Eight times the recording with 30 participants implies roughly 10 to 30 minutes per restart, and restarts are independent processes. A GPU would accelerate only the 30 ms dense residual (a memory-bound weighted GEMM) and the loading solves; the operator construction, sparse assembly, L-BFGS-B kernel loop and objective bookkeeping are Python and SciPy sparse code with no device path. The prior MPS note measured the same loading update as 3 ms on CPU. Recommendation: no GPU work for R. The worthwhile CPU items, if profiling of the real data confirms them, are (a) reusing the reconstruction residual between complete_objective calls inside one alternating iteration, since the objective is evaluated up to a dozen times per iteration on unchanged loadings, and (b) a banded Cholesky (solveh_banded) in place of SuperLU for the latent normal equations. Neither changes the objective.
GP-MSRM grouped likelihood: where the time goes¶
Same synthetic data, linear_algebra="grouped", K = 3, 909 unique (modality, time) nodes, factorization dimension 2,727, 57,757 parameters, 3.64 million observed scalars. bench_gp_parts.py times each stage under jax.jit with warm medians of three.
| Stage | CPU, 64 OpenBLAS threads, load 36, F = 360 | CPU, 16 OpenBLAS threads, load 50 to 65, F = 3,600 | RTX 3090 float64, F = 3,600 | RTX PRO 6000 float64, F = 3,600 |
|---|---|---|---|---|
Covariance over unique pairs and gathers (operands) |
20 ms | 25 ms | 0.9 ms | 0.4 ms |
Block sufficient statistics (segment_sum over observations) |
17 ms | 146 ms | 2.2 ms | 0.7 ms |
| Guarded symmetric solve alone, dimension 2,727 | 2,890 ms | 2,523 ms | 153 ms | 42 ms |
gaussian_terms primal (guarded Cholesky path) |
2,727 ms | 3,223 ms | 145 ms | 42 ms |
| LU reference path | 410 ms | 118 ms | 52 ms | 23 ms |
| Negative log likelihood forward | 154 ms | 227 ms | 59 ms | 26 ms |
| Objective and full gradient | 3,366 ms (8,167 ms at F = 3,600) | 3,720 ms | 172 ms | 70 ms |
| Log prior | 0.06 ms | 0.3 ms | 0.1 ms | 0.2 ms |
Two findings matter more than the device comparison:
- The CPU callback path was pathological here.
symmetric_solveon the CPU platform calls SciPy's POTRF and POTRI throughjax.pure_callback. Direct POTRF+POTRI at this size takes 420 ms with one OpenBLAS thread and 60 ms with 16 threads (potri_threads.py). Through the callback in a JAX process the same work took 655 ms with one thread, 386 ms with 16 threads at load 42, 2.5 s with 16 threads at load 60, and 2.9 s with the default 64 threads at load 36. OpenBLAS threads, XLA's 128-thread pool and the other users' processes oversubscribe the cores, and the callback's serialization with XLA's thread pool amplifies contention. The CI convention of one BLAS thread is safe but slow; 8 to 16 threads is the right region on this machine when it is otherwise idle, and it must be set before Python starts. An uncontended CPU measurement is still needed. The Mac note's 116 ms whole-gradient timing was obtained with one thread on an idle machine, which is why the Linux CPU number looks worse than the Mac number. - The observation-count part is cheap on the GPU and not on XLA CPU. Going from 360 to 3,600 brain features (0.37 to 3.64 million observations) added 4.8 s to the CPU gradient and 30 ms to the 3090 gradient. XLA's CPU scatter-add and its transpose are effectively single-threaded; on the GPU they are memory-bound and negligible. This is the one place where the GPU wins by a large factor even in float64, and it grows with feature count.
Gradient agreement: the objective and gradient fingerprints agreed between CPU and GPU at the float64 level in these runs (differences reported in bench_gp.jsonl). This is algebraic agreement at one parameter vector, not convergence qualification.
GP objective-and-gradient, NUTS and state-space timings¶
bench_gp.py builds the synthetic problem, evaluates the jitted objective-and-gradient at one seeded parameter vector (warm median of five), and optionally runs a short NUTS (15 warmup, 15 draws, diagonal mass matrix, tree depth at most 5) through the package's own sample entry point so that initialization, adaptation, draw conversion, log-likelihood and diagnostics phases are all exercised.
Objective and gradient¶
| Device | K | Recording scale | Dimension | Parameters | Observations | Prepare | Compile + first | Warm gradient |
|---|---|---|---|---|---|---|---|---|
| RTX 3090 | 3 | 1/8 | 2,727 | 57,757 | 3.64 M | 8.9 s | 4.9 s | 196 ms |
| RTX 3090 | 5 | 1/8 | 4,545 | 86,627 | 3.64 M | 7.5 s | 5.4 s | 552 ms |
| RTX 3090 | 5 | 1/4 as one run | 9,205 | 86,627 | 7.28 M | 16.4 s | 13.6 s | 4,653 ms |
| RTX PRO 6000 | 3 | 1/8 | 2,727 | 57,757 | 3.64 M | 7.4 s | 5.0 s | 66 ms |
| RTX PRO 6000 | 5 | 1/8 | 4,545 | 86,627 | 3.64 M | 6.2 s | 6.6 s | 204 ms |
| RTX PRO 6000 | 5 | 1/4 as one run | 9,205 | 86,627 | 7.28 M | 15.6 s | 10.3 s | 1,218 ms |
| RTX PRO 6000 | 5 | 1/2 as one run | 18,530 | 86,627 | 14.56 M | 45.4 s | 25.3 s | 9,308 ms |
| CPU, default threads, load 25 to 35 | 3 | 1/8 | 2,727 | 57,757 | 3.64 M | 9.0 s | 12.7 s | 8,167 ms |
| CPU, 16 threads, load 43 | 3 | 1/8 | 2,727 | 57,757 | 3.64 M | 4.7 s | 3.3 s | 3,201 ms |
| CPU, 16 threads, load 27 (NUTS run) | 3 | 1/8 | 2,727 | 57,757 | 3.64 M | 7.4 s | 5.4 s | 644 ms |
| CPU, 16 threads, load 51 | 5 | 1/8 | 4,545 | 86,627 | 3.64 M | 6.9 s | 13.7 s | 8,420 ms |
| CPU, 16 threads, load 32 | 5 | 1/4 as one run | 9,205 | 86,627 | 7.28 M | 15.7 s | 41.1 s | 33,070 ms |
The objectives agreed between CPU and 3090 to 16 significant digits in all three cases (for example 5323210.5539216 at K = 3) and the sampled gradient entries agreed to about 13 digits. The two K = 3 CPU rows differ by 5x with identical settings and only the background load changed (43 versus 27), which is the clearest evidence that CPU numbers on this shared machine need an idle-machine repeat. Against the better CPU row the 3090 was 3.3x faster at K = 3 and the PRO 6000 9.8x; against the contended rows the 3090 was 16x at K = 3, 15x at K = 5 for the one-eighth case, and 7x at the doubled length. The PRO 6000 was 2.6 to 3.8x faster than the 3090 across the matched cases. On the PRO 6000 each doubling of recording length as one run raised the K = 5 gradient cost 6.0x and then 7.6x, so a full recording as one run extrapolates to roughly 70 s per gradient there, and the 96 GB card held the 18,530-dimension case that the 3090 could not. Doubling the recording length as one run raised the K = 5 gradient cost 8.4x on the GPU and 3.9x on the CPU, consistent with cubic scaling on the GPU and with the CPU still being dominated by the contended callback at the smaller size. Against an idle, well-threaded CPU the expected ratio is 2 to 4x at these sizes, from the raw factorization sweep. Peak GPU memory was not captured by the script (the allocator statistic returned nothing with preallocation disabled); capture it with nvidia-smi sampling in the next campaign.
Short NUTS, K = 3, one-eighth scale¶
| Device | Chains, method | Leapfrog steps | Warmup + sampling | Per leapfrog | Bare gradient in the same process | Initialization | Log-likelihood of draws | Diagnostics |
|---|---|---|---|---|---|---|---|---|
| RTX 3090 | 1, sequential | 465 | 173 s | 0.37 s | 0.17 s | 15 s | 3.4 s | 72 s |
| RTX 3090 | 2, vectorized | 930 | 525 s | 0.56 s per step pair, 0.28 s per chain-step | 0.17 s | 16 s | 7.1 s | 100 s |
| CPU, 16 threads, load 27 | 1, sequential | 465 | 950 s | 2.04 s | 0.64 s | 27 s | 4.6 s | 70 s |
| RTX PRO 6000 | 1, sequential | 465 | 75 s | 0.16 s | 0.065 s | 26 s | 3.4 s | 72 s |
| RTX PRO 6000 | 2, vectorized | 930 | 218 s | 0.23 s per step pair, 0.12 s per chain-step | 0.066 s | 21 s | 7.0 s | 99 s |
The same seed produced the same 465-step trajectory length on all three devices. Per leapfrog step the PRO 6000 was 2.3x the 3090 and 13x the CPU, and vectorizing two chains gave 1.37x throughput on either card. At 15 draws the diagnostics phase already exceeds warmup plus sampling on the PRO 6000. The per-leapfrog figures include NUTS kernel compilation amortized over a short run, so they overstate the steady-state cost; the ratio of leapfrog cost to bare gradient (2.2x on the GPU, 3.2x on the CPU) also includes the diagonal mass-matrix adaptation and NumPyro's tree building, which are serial small operations that suit the CPU less than the dense algebra does. The 3090 was 5.5x faster than the CPU per leapfrog step in this pairing. Sampling ran with XLA_PYTHON_CLIENT_PREALLOCATE=false on a card shared with other jobs. Diagnostics run on the CPU through ArviZ over every parameter, cost about 70 s on either device at 15 draws, and are the second-largest phase.
State-space backend¶
| Device | K | Brain features | Observations | State dimension | Outcome |
|---|---|---|---|---|---|
| RTX 3090 | 3 | 36 | 48,438 | 150 | Out of memory: XLA staged 42.6 GB of scan arguments; allocations of 8 to 20 GB failed |
| RTX PRO 6000 (96 GB) | 3 | 36 | 48,438 | 150 | Out of memory: the objective-and-gradient executable requested a single 213.6 GB allocation for the reverse-mode residuals of the scan |
| CPU | 3 | 36 | 48,438 | 150 | Not run: the same 17.6 GB staging fits in host memory but would occupy the shared machine for the full 25-minute cap to repeat a memory finding |
ResponseStateSpace.prepare gives 50 states per factor for this response configuration (two Gaussian responses on rational or Laguerre banks plus the Matérn pair). StateSpaceSystem.prepare materializes (observations, 150, 150) transition and process-covariance arrays, 17.6 GB at K = 3 and 48.8 GB at K = 5 for this reduced run, and reverse-mode differentiation through lax.scan additionally stores the per-step predicted and filtered covariances, which is what the 214 GB request on the PRO 6000 reflects. Checkpointed differentiation (jax.checkpoint over scan chunks) bounds that residual memory, as the plan's section 7 already requires. At the empirical 3.6 million observations the same arrays would be 1.3 TB. With observations grouped to the 909 unique nodes the arrays are 327 MB and 909 MB. This is the plan's section 7 "exact grouped state-space observation updates" candidate, and it is required before any state-space timing at empirical scale is meaningful, on GPU or CPU. A parallel associative scan would additionally replace 909 sequential 150-dimension updates with a logarithmic-depth combination, which is where a GPU would help this backend.
Scaling to the full recordings¶
Let N be the number of unique observation nodes per run and K the factor count. The grouped algebra forms and factorizes an NK-square dense matrix per run per gradient, and the guarded path also materializes its inverse for the implicit derivative. Costs scale as (NK)^3 in time and (NK)^2 in memory per matrix.
| Case | Nodes per run | Dimension at K = 5 | Dense matrix | Factorization + inverse, RTX 3090 float64 | Same, RTX PRO 6000 float64 | Same on CPU (16 threads, uncontended, extrapolated) |
|---|---|---|---|---|---|---|
| One-eighth (measured shape) | 909 | 4,545 | 165 MB | about 0.5 s | 0.13 s | about 0.4 s |
| One-quarter as one run | 1,818 | 9,090 | 660 MB | about 4 s | about 1.1 s | about 3 s |
| One-half as one run | 3,636 | 18,180 | 2.6 GB | about 30 s (memory-limited) | about 9 s | about 25 s |
| Full recording as one run | 7,272 | 36,360 | 10.6 GB | does not fit | about 70 s | about 3 min |
| Full recording as eight runs | 8 × 909 | 8 × 4,545 | 8 × 165 MB | about 4 s, or 0.5 s per run in parallel | about 1 s | about 3 s |
These extrapolations use the measured n = 2,700 to 16,384 cubic trend (the PRO 6000 column interpolates its measured 4,096, 8,192 and 16,384 rows) and should be replaced by measurements. Their point is structural: with one long run, no device in this machine makes a NUTS chain feasible at full length (a thousand warmup and twelve hundred draws at up to 1,023 leapfrog steps each would need on the order of a year of gradients). With runs at their natural boundaries, the full data costs about eight times the one-eighth case, runs are independent and can be spread over devices, and the GPU speedup of the per-run gradient carries over.
Whether the user's recordings are one run or several is the single most important fact to verify in the plan's Stage A inventory before choosing implementation work. The plan's Stage D duration ladder should be built at run boundaries, as it already says.
What "GPU support" would mean in the package¶
For GP-MSRM the code already dispatches by platform. The remaining work is contractual:
- Installation extra and documentation. Add an optional
bayesian-cudaextra (or documentedjax[cuda13]instructions) with the same pinned JAX version, plus a getting-started section onJAX_PLATFORMS,XLA_PYTHON_CLIENT_PREALLOCATE, thread variables andCUDA_VISIBLE_DEVICES. Keep CPU-only installation the default. - Evidence recording.
execution_infoalready records backend and devices for sampling; MAP diagnostics and fitted archives should record the same, plus dtype, so a fit made on a GPU is identifiable. Warmup checkpoints already bind to device identity. - Numerical qualification on CUDA. Run the existing factorization, analytic-score, multifactor and posterior smoke tests on the CUDA platform and compare objectives, physical gradients and MAP endpoints against CPU at identical parameters, as the plan's Stage C requires. Pin
jax_default_matmul_precision="highest"in the test harness; it only affects float32 but prevents a silent TF32 path if any float32 array ever appears. - Thread and device configuration guard. Emit a warning when the CPU platform runs with more BLAS threads than physical cores or when
pure_callbackis on the hot path with an oversubscribed pool; document 8 to 16 threads as the tested CPU setting on this hardware. - Chain scheduling. Keep
chain_method="sequential"as default; qualifyvectorizedon one GPU (padded trajectories waste work when tree depths differ across chains) and document running separate processes per device for independent chains, each with its own seed and archive. - Memory guard for the inverse path.
symmetric_solvematerializes the full inverse. On a 24 GB card the practical dimension ceiling is about 15,000 to 18,000 in float64; document it and keep the LU reference path as the fallback. A reverse-mode rule that uses solves against the cotangent instead of the explicit inverse would remove that ceiling and reduce work; it is an algebraic change to the derivative rule, not to the density, and would need the existing finite-difference and higher-order derivative tests.
For R-MSRM, no GPU backend. A NumPy array-API port for the two dense operations would touch a third of the fitting code for a few percent of runtime.
Candidate ranking for the plan's Stage C¶
| Track | Candidate | Evidence here | Recommendation |
|---|---|---|---|
| R-MSRM | GPU batched loading or latent solves | Largest dense op 30 ms; Python and sparse code dominate | Reject |
| R-MSRM | CPU residual reuse, banded latent solver, restart parallelism | Profile above | Proceed on CPU after real-data profile |
| GP-MAP | Released grouped path on CUDA | Runs unmodified with 13-digit gradient agreement; gradient 66 ms on the PRO 6000, 172 ms on the 3090, 644 ms to 8.2 s on the contended CPU; 96 GB holds an 18,530-dimension case | Proceed: qualify and document; prefer the PRO 6000 |
| GP-MAP | CPU thread policy fix | 2.9 s to 0.4 s from thread count alone | Proceed first; no GPU needed |
| GP-MAP | Reverse-mode rule without explicit inverse | Inverse path is 60 percent of GPU gradient time and the memory ceiling | Prototype after qualification |
| GP-MAP | State-space with grouped node updates, checkpointed differentiation and associative scan | Per-observation formulation needs 214 GB for the gradient of a 36-feature reduction and fails on both cards; grouped updates cut arrays and scan length by the feature count | Prototype under the plan's section 7 gates; required before any state-space timing |
| GP posterior | NUTS on GPU, one chain per device, CPU chains in parallel | Per leapfrog: 0.16 s PRO 6000, 0.37 s 3090, 2.0 s CPU; vectorized chains gain 1.35x; diagnostics phase is device-independent CPU work | Proceed once MAP is qualified; profile diagnostics separately |
| GP posterior | float32 or mixed precision | Excluded by contract; MPS evidence of gradient failure | Separate labeled study only |
Evidence retained locally¶
Scripts and raw outputs are retained under ignored local_data/linux-performance-v1/2026-09-21-gpu-opportunities/: bench_linalg.py with bench_linalg.log (CPU and partial PRO 6000 rows), bench_linalg_3090.json and bench_linalg_pro6000.json; synth.py; bench_gp.py with bench_gp.jsonl (every objective, gradient, NUTS and error row); bench_gp_parts.py; potri_threads.py; callback_probe.py; profile_r.py with profile_r_F3600.log; run_gp.sh, sequence.sh with sequence.log (3090 and CPU series, including the scale-2 R profile) and sequence_blackwell.sh with sequence_blackwell.log. No empirical data were used; all inputs are synthetic and seeded.
Limits¶
- Synthetic data of the recorded shape, not the empirical arrays; feature count, node count and parameter count were matched from the MPS note's description, not from the actual inventory.
- Contended hardware throughout; the CPU rows especially vary by a factor of two to five with background load. The GPU rows were taken with other users' contexts resident on the cards and are consistent across repeats, but not idle-machine measurements.
- Single parameter vector timings; no optimizer trajectories, no convergence qualification, no posterior diagnostics beyond the short NUTS timing run.
- One run per dataset; the multi-run parallel case is extrapolated, not measured.