Native penalty completion plan¶
- Status: N0–N4 implemented and passed on the fixed GPU host (stage 1,
2026-09-29,
4114918). N5: thepenalty="none"A/B is accepted (frozen-plan recapture at10fc363, 2026-09-30) and the native CUDA L1 baseline is recorded; PyTorch CUDA DLPack passed on the device on 2026-10-03 at33cb075(see "N5 fixed-host results" and "Still open" at the end) - Baseline:
v0.6.1/ CUDA C ABI 1 / CUDA Python API 3 - Scope: complete the existing public penalties,
noneandl1 - Out of scope: adding L2, elastic-net, reduced precision, or automatic CUDA selection
Outcome¶
The public estimator already defines penalty as none | l1. Native CPU
implements both paths. Native CUDA implements only none and correctly rejects
l1 before entering the extension. This work closes that CUDA gap, strengthens
the native CPU oracle coverage, and makes penalty support discoverable before an
update starts.
Completion means all three engines can resume the same L1 checkpoint and process the next batch with equivalent state, diagnostics, and predictions:
No phase may change the paper equations, batch-order semantics, frequency-weight semantics, intercept exclusion, strict dtype behavior, or public estimator API.
Frozen decisions¶
- Keep
tests/golden/native_core_v1.jsonbyte-identical. Add a v2 corpus; do not regenerate v1. - Generate and review the v2 expected values from the NumPy reference at the baseline commit before using any candidate native engine as an oracle.
- Keep checkpoint format 2. It already stores the config,
previous_lambda,weight_sum, coefficients, and information matrix required by L1. - Raise the CUDA C ABI from 1 to 2 and the CUDA Python payload API from 3 to 4.
New and old components must fail closed instead of silently treating L1 as
none. - Preserve exactly 17 exported
rh_cuda_*symbols. Evolve the existing update contract; do not add a second versioned update symbol. - Change
native/contracts/rh_cuda_contract.jsonfirst, then update every mirror named by that manifest. Preserve all parser-count assertions. - Native CUDA remains explicit opt-in. Functional L1 support does not authorize
backend="auto"to select CUDA. - GPU correctness and performance run on the fixed local GPU host, not GitHub Actions. Portable ABI, Rust, Python, build, and CPU tests remain in CI.
Numerical contract¶
For total effective weight N, feature count p, Huber threshold tau, and
configured scale lambda_scale:
The CUDA L1 transition must match the reference and Rust CPU behavior:
- include the historical quadratic term from the information matrix;
- include the historical subgradient correction using the previous state's
weight_sum,previous_lambda, andsign(coefficients); - apply soft thresholding to every coefficient except the trailing intercept;
- use the existing 40-attempt LAMM backtracking rule,
phiupdate, tolerance, and convergence definition; - report the final penalized diagnostic objective and current lambda;
- commit the current lambda to
previous_lambdaonly after a successful, transactional update; - form final renewable information at the accepted coefficients;
- leave active device state unchanged after any validation, CUDA, or solver failure.
Strict float32 and float64, host input and zero-copy CUDA DLPack input,
weighted and unweighted batches, and intercept/no-intercept configurations are
all part of the contract.
Work stages¶
The names N0-N5 avoid the repository's two existing P0-P3 numbering schemes.
N0 — Freeze oracle and compatibility boundaries¶
Deliver:
native_core_v2.json, generated from baseline NumPy, with at least:- weighted three-batch L1 float64, including zero and non-unit weights;
- multi-batch L1 float32 without an intercept and with true zero coefficients;
- sparse L1 float64 with an intercept that must remain unpenalized;
- streaming L1 with
lambda_scale=0; - tests that replay v1 and v2 independently;
- an ABI 2 contract proposal covering a fixed-width penalty enum and
lambda_scale; generate_native_golden.py --checkgates for both corpora.
Accept when v1 is byte-identical, v2 is deterministic, and NumPy can replay every case twice with identical serialized output.
N1 — Shared contract, capabilities, and fail-closed ABI 2¶
Deliver:
- CUDA update config generalized from unpenalized-only to
none | l1; - fixed-width
NONE=0,L1=1C ABI values andlambda_scale; - manifest, C header, C++ assertions, Rust layout, PyO3 boundary, and version metadata updated together;
- CUDA Python API 4 metadata with
supported_penalties; - backend capability field
native_update_penaltiespopulated only throughcapabilities_of(): - native CPU:
{none, l1}; - old CUDA API:
{none}; - new CUDA API:
{none, l1}; - explicit requests for an unsupported penalty fail before native execution;
rh-cuda-ffireusesrh-corepenalty/config validation where it reduces duplicated policy without coupling CUDA buffers to CPU implementation types.
Accept when malformed penalties, ABI 1/API 3 mismatches, and partial struct layouts all fail closed; the library still exports exactly 17 symbols; all no-CUDA ABI/layout tests run in CI.
N2 — CUDA L1 transition¶
Deliver the L1 solver behind the existing whole-batch native update boundary. The implementation may reuse or extend the current trial, candidate, gradient, delta, reduction, and information workspaces. Its internal kernel split is left to the implementer, subject to the numerical and transactional contracts above.
The first correct implementation may use ordinary stream launches and decline CUDA Graph capture for L1. Graph specialization is an optimization, not a functional acceptance requirement.
Accept when direct C ABI smoke tests cover host/device input, weighted streaming,
intercept masking, lambda_scale=0, invalid config, restored asymmetric
information, non-convergence, and failure rollback for both dtypes.
N3 — Bindings and cross-backend resume¶
Deliver:
- PyO3 host and DLPack updates that pass penalty and lambda policy explicitly;
- removal of the Python native-CUDA L1 rejection only after API 4 support is verified;
- native CUDA replay of every v2 L1 case;
- NumPy -> CPU -> CUDA and CUDA -> NumPy/CPU checkpoint migration tests;
- immediate prediction and next-batch
partial_fitafter restore; - no checkpoint format or public estimator API change.
Accept when a checkpoint taken after any v2 batch can resume on each other engine and match the uninterrupted oracle within the existing dtype-specific tolerances for state, diagnostics, and predictions.
N4 — Full correctness and regression matrix¶
Required gates:
- Python
core,native-cpu,cuda, andallprofiles; - all Rust formatting, clippy, check, and scoped tests;
- C++ ABI syntax check, static CMake build, and CTest;
- CUDA C ABI status-survival test across translation units;
- exact 17-symbol export check and all contract parser counts;
- v1
noneresults unchanged and v1/v2 three-engine differential replay; - host and DLPack L1, float32/64, weighted/unweighted, intercept/no-intercept, checkpoint resume, NaN/Inf, and rollback tests.
No test may convert a missing native dependency or GPU into success in a required profile.
N5 — Benchmark, documentation, and release readiness¶
Deliver:
- shape-sweep native CUDA runner accepts L1 instead of hard-coding
none; - removal of the explicit CUDA-L1 benchmark skip;
- fixed-host benchmark records separated by host/device transport, dtype, lifecycle, operation, shape, and penalty;
- interleaved A/B protection showing the existing CUDA
nonepath has not regressed; - new L1 comparisons against CuPy under the same input-transport contract;
- support matrix, architecture, P2 history, CUDA package README, performance policy, changelog, and release checklist updated together.
For existing none, use at least nine aligned paired samples, require all cases
to converge, median iteration difference at most one, relative MAD at most 10%,
and candidate slowdown at most 1.15x. Do not claim differences inside the
host's documented approximately 10% noise band.
There is no previous native CUDA L1 baseline. Initial L1 acceptance therefore requires correctness plus stable measurements. Keep it explicit opt-in unless the same-transport native result demonstrates a repeatable advantage over CuPy; freeze that accepted result as the baseline for later releases.
Implementation status¶
Implemented on claude/nifty-goldberg-0lm77u (base fca7b83):
- N0.
tests/golden/native_core_v2.jsonholds the four required L1 cases, generated byscripts/generate_native_golden.py --corpus v2from the NumPy reference and checked twice for byte-identical output. Every v2 case converges (tol=1e-6; tighter tolerances make LAMM stall atmax_iter, which is a poor cross-engine oracle). v1 is byte-identical tov0.6.1; CI's--checkof both corpora is what keeps them that way. - N1. C ABI 2 / Python API 4.
RhCudaUnpenalizedConfigbecameRhCudaUpdateConfig(72 bytes:penalty,reserved0,lambda_scaleappended);RH_CUDA_PENALTY_NONE=0,RH_CUDA_PENALTY_L1=1. Unknown penalties, a non-zeroreserved0and a negative or non-finitelambda_scalefail before device work.rh-cuda-ffivalidates throughrh_core::UpdateConfig::validate.native_update_penaltiesis resolved only bycapabilities_of(): NumPy/portableNone, native CPU{none, l1}, native CUDA whateverversion()["supported_penalties"]advertises, and{none}when it advertises nothing. The core refuses an unadvertised penalty withValidationErrorbefore the engine is called. The library still exports 17 symbols;rh_cuda_engine_predict_hostwas renamedrh_cuda_engine_predictas part of the ABI 2 break (see below). - N2.
solve_l1innative/cuda/src/pipeline.cumirrors the Rust CPU engine step for step. The historical subgradient term and the majorization inner product ride the existing single objective transfer (reduction slots 4 and 5); the final L1 norm rides the update's one transactional sync. CUDA Graph capture is supported for the L1 candidate objective as well.previous_lambdais committed only after the transactional swap. - N3. PyO3
update/update_devicetakepenaltyandlambda_scale; the Python L1 rejection is gone;tests/test_native_cuda_backend.pyreplays v1 and v2 and resumes an L1 checkpoint NumPy -> CUDA -> CPU. - N4. Portable gates (contract referee, Rust layout, fakes, NumPy and native CPU v2 replay) run in CI. The CUDA smoke test gained five cases (reference L1 stream, unpenalized intercept, rejection rollback, device vs host L1, device prediction).
Beyond the plan, the same ABI break carries device-resident prediction:
RhCudaHostPrediction became RhCudaPrediction with n_features_in and an
input_location (RH_CUDA_MEMORY_HOST/RH_CUDA_MEMORY_DEVICE), so a CUDA
DLPack tensor is read in place and the raw feature matrix may be widened on
device, exactly as for updates.
N5 fixed-host results¶
Stage 1 and stage 2 of the GPU host runbook ran on
2026-09-29 at 4114918, the first time this implementation executed on a
GPU. The §3a A/B was recaptured on 2026-09-30 at 10fc363, after #50 and #51
made an A/B across the ABI change measurable; that recapture is the accepted
result. Section numbers such as §3a refer to the runbook.
| Component | Version |
|---|---|
| GPU | NVIDIA GeForce RTX 5070 Ti, compute capability 12.0, WDDM |
| Driver | 616.64 (CUDA UMD 13.4) |
| CUDA toolkit | 12.9 (nvcc 12.9.41) |
| Host compiler | MSVC 19.43.34809 (VS 2022 Build Tools 17.13) |
| Build tools | CMake 3.29.2, Ninja 1.12.0, rustc 1.97.1 |
| Python stack | CPython 3.11.0, NumPy 2.4.6, SciPy 1.17.1, cupy-cuda12x 14.2.0 |
Native CUDA version() |
ABI 2, Python API 4, runtime 12090, driver 13040, supported_penalties [none, l1] |
Stage 1: correctness on the device¶
Every stage 1 command passed:
| Command | Exit | Result |
|---|---|---|
run_test_profile.py --check |
0 | 6 profiles cover 25 test modules |
run_test_profile.py core --verbose |
0 | Ran 326, OK (skipped=2) |
run_test_profile.py native-cpu --verbose |
0 | Ran 20, OK |
run_test_profile.py cuda --verbose |
0 | Ran 40, OK (skipped=2) |
run_test_profile.py performance --verbose |
0 | Ran 54, OK |
generate_native_golden.py --check and --corpus v2 --check |
0, 0 | "Golden corpus matches 4 generated cases" for each |
static CMake build and ctest |
0 | 1/1 passed; ctest --verbose shows rh_cuda_smoke passed, which the binary prints only after all 13 named cases, including the four l1_* cases and prediction_reads_device_input |
shared build and dumpbin /exports |
0 | exactly 17 rh_cuda_* exports |
python -m build --wheel |
0 | base wheel built |
smoke_test_cuda_wheels.py |
0 | "Clean wheel smoke passed" in a clean venv with the pip nvidia-*-cu12 12.9 runtime wheels |
The two skips in core and in cuda are the PyTorch and TensorFlow CUDA
DLPack integration tests in tests/test_dlpack_adapters.py. Neither framework
is installed by the runbook's extras, so those two device paths were not
exercised. The PyTorch path has since run on the device; see
PyTorch CUDA DLPack on the device.
PyTorch CUDA DLPack on the device¶
On 2026-10-03 at 33cb075, on the same host and venv as stage 1 (Windows 11,
CPython 3.11.0, driver 616.64, CUDA 12.9 / nvcc 12.9.41), a CUDA 12.9
PyTorch build was installed and the cuda profile rerun, as the runbook's
stage 1 now requires before a release:
| Check | Result |
|---|---|
pip install torch --index-url https://download.pytorch.org/whl/cu129 |
exit 0, torch 2.9.0+cu129. It added only filelock 3.32.3, fsspec 2026.7.0, Jinja2 3.1.6, MarkupSafe 3.0.3, mpmath 1.3.0, networkx 3.6.1 and sympy 1.14.0; pip freeze before and after shows no existing package changed |
torch.__version__, torch.version.cuda |
2.9.0+cu129, 12.9 |
torch.cuda.get_device_capability(), is_available() |
(12, 0), True, NVIDIA GeForce RTX 5070 Ti |
native CUDA version()["runtime_version"] |
12090 on its own, and still 12090 (driver 13040, ABI 2, API 4) when torch is imported first |
run_test_profile.py cuda --verbose |
exit 0, Ran 40, OK (skipped=1) |
PyTorchDlpackIntegrationTests.test_cuda_tensor_matches_host_input_and_detaches_autograd
passed: a native CUDA float32 fit on PyTorch CUDA tensors that require
gradients matches the same fit on host NumPy input. The one skip is
TensorFlowDlpackIntegrationTests.test_eager_cuda_tensor_matches_host_input_without_storage_copy
("native CUDA API 4 and eager CUDA TensorFlow are required"), which is expected
on native Windows.
PyTorch for Windows ships its CUDA runtime as DLLs in torch\lib and installs
no nvidia-*-cu12 wheels, so renewable_huber._cuda_runtime finds no
complete nvidia-*-cu12 set in this venv and falls back to the system CUDA
12.9 toolkit. The runbook's check is that the extension still reports runtime
12090 with or without torch imported first, and it does. pip check in this
venv lists the native CUDA wheel's nvidia-*-cu12 requirements as missing.
That predates PyTorch: the development build script installs the wheel with
--no-deps. The clean-venv wheel smoke in stage 1 is what exercises the
nvidia-*-cu12 runtime.
Stage 2 §3a: penalty="none" A/B, accepted¶
Verdict: §3a is accepted. fca7b83 and 10fc363 show no measurable
difference for penalty="none". The official result is the frozen-plan
recapture of 2026-09-30. The two earlier attempts of 2026-09-29 are
superseded and listed at the end of this section.
Baseline fca7b83 (ABI 1 / API 3) was compared with candidate 10fc363
(ABI 2 / API 4). With --freeze-sample-repetitions, both sides run the
candidate's measurement harness against their own source tree. The baseline
records therefore carry git_revision fca7b83 and
benchmark_harness_git_revision 10fc363. The candidate records have no
harness field, because the candidate measured its own tree.
Both venvs were the ones from 2026-09-29: CPython 3.11.0 with the same NumPy,
SciPy and CuPy versions, and Release native CUDA builds for SM 120. Nothing was
rebuilt. No native library code changed between 4114918 and 10fc363, so
the candidate extension built earlier is the one that was measured.
The command was the runbook §3a command: --profile standard --backend gpu
--penalty none --dtype both --lifecycle cold --operation partial-fit
--rounds 9 --allow-native-version-change --freeze-sample-repetitions, with
every other argument at its default. The steady run was identical apart from
--lifecycle steady --output-dir artifacts/n5-none-ab-steady. Each lifecycle
gates 16 native cases: two transports, four shapes and two dtypes.
| cold | steady | |
|---|---|---|
| window (+08:00) | 16:37:44–16:41:41 | 16:42:07–16:46:34 |
| wall time, exit code | 236.3 s, 0 | 266.9 s, 0 |
gate.json passed |
true |
true |
schema_version, checked_cases |
2, 16 | 2, 16 |
| failure reasons | none | none |
allow_native_version_change |
true |
true |
native_versions.native_cuda_abi |
ABI 1 / API 3 → ABI 2 / API 4, changed: true |
same |
sample_repetitions.policy, .harness |
frozen_plan, candidate |
frozen_plan, candidate |
sample_repetitions.plan_sha256 |
d6a026f2405b089bc8739d2771337d00c02b4305db711bad1cd92f3f472a04f3 |
02a4d3dfb5961c412dc0d2c24d7ad08677e1f0ca6601f7cece83b6a02cf74405 |
| plan block sizes (32 cases) | 1–58 | 3–64 (four cases at the default cap of 64) |
Checks on the frozen plan:
- Each
plan_sha256equals the SHA-256 of its plan file. - The plan is the per-case maximum of the two calibration sweeps.
- All 18 round records of each run (nine per variant) used the plan, and every case kept the same repetition count in every round.
- The merged records' repetitions equal the plan.
Recomputing the gate: compare_interleaved_records and report on
main were run on the committed baseline and candidate records with
allow_native_version_change=True. They reproduce the committed gate.json
exactly. Without the option, every case reports only
hardware or runtime fingerprint differs.
The plan's five criteria, evaluated per case:
| Criterion | cold | steady |
|---|---|---|
| at least nine aligned pairs | 16/16 (9 each) | 16/16 (9 each) |
| every case converges | 16/16 | 16/16 |
| median iteration difference ≤ 1 | 16/16 (all 0) | 16/16 (all 0) |
| relative MAD ≤ 10% on both sides | 16/16 (max 8.3%) | 16/16 (max 8.5%) |
| candidate slowdown ≤ 1.15x | 16/16 | 16/16 |
| all five | 16/16 | 16/16 |
| paired median, candidate / baseline | 0.937–1.056 | 0.925–1.038 |
| median of medians, candidate / baseline | 0.947–1.055 | 0.982–1.081 |
| native / CuPy, same transport (gate limit 1.0) | 16/16, 0.481–0.862 | 16/16, 0.349–0.838 |
Reading the results:
- Every difference is inside ±10%, so there is no measurable difference
between the two engines for
penalty="none". - No threshold was changed, and none needed to be.
- The steady device-input reference float32 case, 1.111 in the superseded
--max-sample-repetitions 1run, measured 1.015 here. That earlier observation did not repeat. - The ungated CuPy control runs the same CuPy code on both sides. Its relative MAD reached 13.1% (cold) and 14.0% (steady), and its paired ratios spanned 0.883–1.129. The gate applies the MAD limit to the gated native engines only.
Environment: nvidia-smi --query-compute-apps listed the same GPU
clients, with the same process IDs, before and after both runs:
- Wallpaper Engine, Chrome, Discord, NVIDIA Overlay, the Claude desktop app
(
claude.exe) and Microsoft Edge WebView2; - Riot Vanguard (
vgtray.exe), AMD Radeon Software (AMDRSSrcExt.exe) and Phone Link; - Windows shell processes.
They were not closed, because the maintainer uses this computer during measurements. No other Python, Cargo, CMake or CUDA build process was running.
Committed records. benchmark_shape_sweep.py/run_interleaved_benchmark.py
write records and gate.json with CRLF on Windows, and .gitattributes
stores them with LF; that is the only difference. The plan files are written
as bytes with LF, so their hash is unchanged and still equals plan_sha256.
| Record | SHA-256 on the host | SHA-256 committed |
|---|---|---|
p5-windows-rtx5070ti-native-cuda-none-ab-cold-baseline.json |
267304f0eb657a4787aeabcdf98ae6e3d89789598071f50217cd9cbad56c0963 |
2349b80df16331a507a64671c6e231dbfb027f44dc9e0e2f8beafc828435f62c |
p5-windows-rtx5070ti-native-cuda-none-ab-cold-candidate.json |
9ed4a927cfdd484f6230694ddd44fc863b9264aecc392e390b832e867d3cd9f1 |
58ec01945db8d08bd6131443cdd2faafd6fd039dd030aea41e24028c8b8e6cad |
p5-windows-rtx5070ti-native-cuda-none-ab-cold-gate.json |
800abda85341d999a53bb187ed64618a3331daae71f90ec873c8972930fc382c |
c9a706652ffa6b5921837f8f7cedd250e4be315b41d23cf5ddbd6fb5e4075b3f |
p5-windows-rtx5070ti-native-cuda-none-ab-cold-sample-repetitions-plan.json |
d6a026f2405b089bc8739d2771337d00c02b4305db711bad1cd92f3f472a04f3 |
same |
p5-windows-rtx5070ti-native-cuda-none-ab-steady-baseline.json |
7519364df6d7659b847fde48ee26fd446d9c6f4dc700459edd5c656ab0a4ee49 |
234cd3d807b07e8bb8b2079a6b94cbdc59c4cb23d2455f5e2f82c1c6987b7e4c |
p5-windows-rtx5070ti-native-cuda-none-ab-steady-candidate.json |
097c33305224205ca4ecbc7ce18dc7a78f85668a39bbd90a3edc9205416c7bbd |
a029daec76b8d8cbd7ad45de6be572df6554135b44df5e179cf76c3b9d2045a9 |
p5-windows-rtx5070ti-native-cuda-none-ab-steady-gate.json |
41583d00ea2f6cf6be5a0b2c57bf1aab4c679c8efcc5ed0b4255e009ba75b054 |
a845e0c8877e5e6e1edd562a86ee9c6156e2ea9aee184b0ea2d427df84d1b55e |
p5-windows-rtx5070ti-native-cuda-none-ab-steady-sample-repetitions-plan.json |
02a4d3dfb5961c412dc0d2c24d7ad08677e1f0ca6601f7cece83b6a02cf74405 |
same |
validate_record on main accepts all four committed baseline and candidate
records.
Kept on the host, not committed, under artifacts/n5-none-ab/ and
artifacts/n5-none-ab-steady/:
- the two calibration sweeps of each run. Their SHA-256 are: cold baseline
91809d09b96539f268c551f6c7f4766f4932c0cfb74c5b80f9d589b1cc92d6ac, cold candidate965fda24136ebb7b8d062e8def49af56c9f4b0b4fcdf1e4e9b6eea0fb5607227, steady baselinee188cf3312670abad7af521134e2033064053aa476727cdc39ff5c701cf506d3, steady candidatec5e6c74fee30b919ff33ac8a63cdd7d79ea766e13506d2d4fd18706f0ea832ab; - the 18 round records of each run, in
rounds/. Their SHA-256 are in the description of pull request #49.
Superseded attempts (not accepted)¶
These are kept on the host for the record. None of them is evidence for or against a regression.
- Default repetition cap, 2026-09-29 at
4114918. Each round calibrated its own sample blocks. After nine roundsmerge_round_recordsrefused to merge them ("sample repetition calibration changed between interleaved rounds"), so no gate report was written. The round records are inartifacts/n5-none-ab.attempt1-default-reps/andartifacts/n5-none-ab-steady.attempt1-default-reps/. Wallpaper Engine was running for the cold attempt and closed during the steady one. --max-sample-repetitions 1, 2026-09-29 at4114918, without--allow-native-version-change. The records merged, but both gates saidpassed=false, for two reasons:- Every case reported
hardware or runtime fingerprint differs. The only differing fields werenative_cuda_abi.abi_versionandpython_api_version, which is what #50 addresses. - With single-operation samples, relative MAD exceeded 10% in one cold case and two steady cases, all device-input float32: cold streaming (baseline 10.4%), steady latency (baseline 10.9%, candidate 19.1%) and steady streaming (candidate 10.4%).
Otherwise the plan's criteria held. Paired medians were 0.921–1.045 cold and 0.986–1.111 steady; the 1.111 was steady device-input reference float32. Native/CuPy was 0.27–0.82.
OpenAI Codex (ChatGPT.exe, codex-computer-use-swift.exe) appeared as a
GPU client from 16:40, during the steady run.
The records have moved to artifacts/n5-none-ab.reps1/ and
artifacts/n5-none-ab-steady.reps1/, with unchanged hashes:
| Record | SHA-256 |
|---|---|
n5-none-ab.reps1/baseline.json |
ebf14fce8545f8323ce10e97ad238e13129f6bbb2067c1b2b3fba95bfeb902b3 |
n5-none-ab.reps1/candidate.json |
9e73bc182935ecf0b787e404063551a3af6362030e19a1327030a30774324dea |
n5-none-ab.reps1/gate.json |
b8092da595696f3f6c7188479fc3fc637c43e6369c2e0251f88fe4e6bb9041ff |
n5-none-ab-steady.reps1/baseline.json |
b270a2e7d8fb35db8e8ec9ac4bf48e3d26a62b9cc7e9e470a7d80e234d945b84 |
n5-none-ab-steady.reps1/candidate.json |
ad1c85d0aed1a9921ab73cb9cdb31890568d2ce483bf93312d66b86dc8191207 |
n5-none-ab-steady.reps1/gate.json |
707942a96d594ad4e07da539e74342218b33f522d114c8dcc3509fbde8170bd0 |
Stage 2 §3b: L1, native CUDA against CuPy¶
The runbook command ran unchanged three times, back to back. Each run holds
96 cases: native CUDA and CuPy, each with host and device input, over four
shapes, two dtypes, and cold fit, cold partial_fit and steady
partial_fit. Every case converged. Each run also lists 24 skips, all
"fit resets the estimator; steady state is partial_fit only". The runs took
place 16:42–16:50 (+08:00) on 2026-09-29, with Wallpaper Engine closed and
OpenAI Codex (ChatGPT.exe, codex-computer-use-swift.exe), Chrome,
Discord, Obsidian, NVIDIA App and NVIDIA Overlay running.
That gives 48 native-against-CuPy comparisons with shape, dtype, lifecycle, operation and transport held equal. Classified by the ratio of the native median to the CuPy median in each of the three runs:
| Category | Comparisons |
|---|---|
| ratio < 0.90 in all three runs (repeatable native advantage) | 46 |
| ratios within 0.90–1.10 (no measurable difference) | 2 |
| ratio > 1.10 in all three runs (repeatable native disadvantage) | 0 |
Across the 46, the per-run ratios run from 0.23 to 0.89 with a median of
0.34; 38 of the 46 are below 0.5 in every run. The advantage is smallest for
cold fit of the streaming shape and of reference float64. The two
comparisons with no measurable difference are both host-input streaming cold
fit: float32 at 1.055–1.071 and float64 at 0.902–0.978.
The largest native relative MAD in any run is 9.4%. On the CuPy side, three
rows exceed 10% (24.1%, 10.3%, 10.5%). The native run-to-run spread, the
largest over the smallest of the three native medians, is 1.04–1.49. It
exceeds 1.2 only for reference float32 partial_fit (all four
lifecycle/transport combinations, 1.22–1.49) and host-input latency float64
cold partial_fit (1.26).
The float32 L1 partial_fit iteration counts (summed over the batches of the
stream) differ between native CUDA and CuPy; each engine's count is the same
in all three runs. The float64 counts match everywhere:
Shape (float32 partial_fit) |
native CUDA | CuPy |
|---|---|---|
| reference | 29 | 26 |
| streaming | 52 | 63 |
| wide | 49 | 51 |
(Wide float32 cold fit differs by exactly one, 14 against 15.) A follow-up
diagnostic replayed the same shape-sweep datasets through all four engines
with penalty="l1", tol=1e-6, and compared each float32 result with a
float64 NumPy fit of the same float32-valued data. The float32 iteration
counts differ across all four engines, NumPy included (streaming: NumPy 55,
native CPU 56, CuPy 63, native CUDA 52). Every engine converged, and every
float32 result lies within 8.2e-7 to 4.4e-5 of the float64 reference (largest
coefficient difference relative to the largest reference coefficient). Native
CUDA is among the closest (8.2e-7 reference, 2.8e-6 streaming, 7.1e-6 wide),
and its host-input and DLPack results are identical. The differing counts are
float32 rounding near tol=1e-6, not an engine defect.
The three runs are committed as the fixed-host L1 baseline.
benchmark_shape_sweep.py writes them with Windows CRLF line endings;
.gitattributes stores them with LF, which is the only difference from the
files on the host:
| Record | SHA-256 on the host (CRLF) | SHA-256 committed (LF) |
|---|---|---|
p5-windows-rtx5070ti-native-cuda-l1-run1.json |
41ff3e17360558251c8eeb37649dfd39c3fbf6c3ec1f8a919fc5c5b415f0f1ee |
ae1b52a0b2863e248d305d56df7ca79f82dac18ae8efaf711ed86ce45ce9e48b |
p5-windows-rtx5070ti-native-cuda-l1-run2.json |
de61ce2eedfee13a2a0770fc1aa21e77e4e165c6b6000d46248bf001f5c4cf26 |
e18537cbf99c03795c9bca74da3dc2483b7511678d8961805559dbe4838eed36 |
p5-windows-rtx5070ti-native-cuda-l1-run3.json |
60b9d45911c04a66885f13918446cd545642b5c92320ce4bc7e0d0d61939b333 |
08ae8e9097af45701e0ac5d2e3392adb49d5c22fac9909228eea41faa85d811b |
Since #50, validate_record accepts native CUDA L1 records whose recorded
native_cuda_abi shows ABI 2 or later or lists l1 in
supported_penalties. It accepts all three committed runs (ABI 2 / API 4).
Decision: native CUDA L1 stays explicit opt-in¶
Native CUDA L1 stays explicit opt-in; backend="auto" never selects it.
- The condition this plan and the runbook set for considering more than opt-in, a repeatable same-transport advantage over CuPy, is met on this host: 46 of 48 comparisons, two with no measurable difference, none worse. That is recorded here as evidence.
- It still does not change selection. Frozen decision 7 and this plan's
scope exclude automatic CUDA selection, and
autodoes not select native CUDA even forpenalty="none", where native CUDA also beats CuPy (0.35–0.86 in the accepted §3a recapture). Automatic CUDA selection is a question independent of the penalty. It needs its own RFC, as the CPU path had in the CPU auto-dispatch RFC, with evidence from more than one host. - The
penalty="none"A/B is now accepted (no measurable difference betweenfca7b83and10fc363). That removes one earlier reason for caution, but not reasons 2 and 4, so the decision stands. - The float32 L1 trajectories differ between engines: the results are numerically equivalent, but the iteration counts are not. Switching engines silently would change the diagnostics users see.
Still open¶
Resolved since the first run:
- the §3a acceptance, by #50 (
--allow-native-version-change), #51 (--freeze-sample-repetitions) and the accepted recapture above; - consuming the L1 baseline, by #50's conditional native CUDA L1 rule in
validate_record; - PyTorch CUDA DLPack, which stage 1 skipped because PyTorch was not
installed: with
torch2.9.0+cu129 in the venv,PyTorchDlpackIntegrationTestspassed in thecudaprofile on 2026-10-03 at33cb075, and the native extension still reported runtime 12090. See PyTorch CUDA DLPack on the device.
Still open:
- TensorFlow CUDA DLPack is released as unverified on a device. TensorFlow has no GPU support on native Windows since 2.11, so the Windows host cannot run it; it needs a WSL2 or Linux GPU host. The support matrix says so.
Resolved after the evidence PR: the offline dispatch advisor
(scripts/benchmarks/dispatch_policy.py) still recommends native CUDA only
for penalty == "none", which matches the opt-in decision above, and its
comment now gives that reason instead of the pre-ABI 2 "does not implement
L1"; the shape-sweep CLI docstring no longer names a native CUDA L1 skip that
ABI 2 removed.