Repository navigation
fix(speed): GPU SpEED correctness + safety across all backends (audit) - #1029
Merged
lusoris merged 11 commits intoJun 20, 2026
Merged
Conversation
…kends) + SYCL solve-barrier deadlock
Three real bugs in the SpEED extractors, found via container ASan + GPU-fault
debugging. The eigenvalue-scratch one is retrain-critical (corrupts the heap
during GPU speed extraction on the RTX 4090).
1. CPU speed.c heap-buffer-overflow (this was the CI all-backends SEGV).
speed_init_dimensions() returns -EINVAL for planes too small for the
NUM_SCALES pyramid (e.g. YUV420 chroma below ~160px), but speed_init() and
init_chroma() IGNORED the return -> submatrix_height = truncated_height -
block_size + 1 underflowed the size_t to ~2^64 -> compute_mean() walked off
the frame buffer. The SYCL twin already checked this return; the CPU path
did not. Fix: reorder the truncated==0 guard before the submatrix subtraction
and propagate the return through speed_init() + init_chroma(). ASan-clean.
2. GPU eigenvalue-scratch heap corruption (RETRAIN-CRITICAL; all 6 GPU speed
extractors: cuda/hip/sycl x chroma/temporal). speed_internal_compute_
eigenvalues() lays its scratch out as A[n*n] + d[n] + sd[n] + tmp[2*n] =
n*n + 4*n floats, but every GPU caller allocated n*n + 3*n -> si_tri_multiply
wrote n (=25) floats past the end -> 'free(): invalid next size'. On normal
heaps this is a silent 100-byte overwrite; under MALLOC_PERTURB/ASan it
crashes. The CPU path uses its own correctly-sized eig buffer. Fixed all 6.
3. SYCL solve-kernel divergent-barrier deadlock (DEVICE_LOST on strict-barrier
GPUs, e.g. Intel Arc). launch_solve early-returned idle lanes (lane >=
SP_ELEMENTS) and surplus warps before a work-GROUP group_barrier they were
required to reach -> the group deadlocked. Fix: gate the work with an
flag, keep all work-items in the barrier loop. Applied to
speed_chroma_sycl + speed_temporal_sycl.
Also enlarges test_sycl_speed_chroma_parity's fixture (256x144 -> 320x320): the
old fixture's chroma plane (128x72) is below the SpEED minimum, so the extractor
EINVAL'd and the test never actually ran speed_chroma; it was masked while
speed_chroma_sycl was unregistered (pre-#1004).
Verified in the dev container: ASan-clean on the SYCL CPU-OOB repro and on the
full Arc-GPU speed_chroma pipeline (means/cov/indterm/solve/score all pass,
no heap corruption, no DEVICE_LOST).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e picture_copy speed_chroma_cuda extract_channel ran host-side picture_copy() on ref_pic->data[channel], which is a DEVICE pointer in the CUDA pipeline (like adm/vif_cuda) -> reading GPU memory on the host SEGVs every frame. Never caught (CUDA tests need a GPU; CI has none). Download via cuMemcpyDtoH into host staging, alias into a temp VmafPicture, then picture_copy. Verified on RTX 4090: SEGV gone, parity runs. A separate GPU-algorithm score discrepancy (~7x low vs CPU) remains, tracked separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…_cuda Same device-pointer SEGV as speed_chroma_cuda: extract_fex_st ran host-side picture_copy() on ref_pic/dist_pic->data[0], which is a CUdeviceptr in the CUDA pipeline -> host read of GPU memory SEGVs every frame. Download via cuMemcpyDtoH (with a local cuCtxPushCurrent, since the GPU pipeline pushes the context later) before the host picture_copy. Verified on RTX 4090: SEGV gone, parity runs (a shared GPU-algorithm score discrepancy remains, tracked separately). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-tile speed_means_kernel + speed_cov_kernel computed PER-TILE block-local statistics (means[elem*num_blocks+tile], data sampled at tile_y*5+er) and summed num_blocks displaced block-local covariances, while the CPU reference computes ONE GLOBAL covariance per (x,y) element pair over a 5x5-phase-shifted full-plane submatrix (means[elem], data at er). The GPU port used the wrong sampling decomposition and was never validated (CUDA tests need a GPU; CI has none), giving ~7x-wrong SpEED scores on every GPU run. Rewrite both kernels to the CPU's global formulation: means kernel = 25 threads, each the global mean at (er,ec); cov kernel = global submatrix sweep with scalar global means, divide by N once, no tile loop. means[] stays over-allocated (25*num_blocks); only [0,25) used. Launch geometry unchanged. Verified on RTX 4090: test_cuda_speed_temporal_parity now PASSES bit-parity vs CPU (was wrong); test_cuda_speed_chroma_parity improved from cpu=19.84/cuda=2.89 (7x low) to within 2x — the residual is a SEPARATE chroma bug (the CUDA score kernel shares one eigenvalue/cov basis for ref+dis via save/restore, but the CPU uses separate ref/dis bases; matters only when ref!=dis, i.e. chroma not temporal). Tracked as a follow-up. SYCL/HIP twins carry the same kernel-geometry bug — to be ported next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nit fail_pop - speed.c temporal init ignored speed_init()'s return; on -EINVAL (in-range-but- invalid speed_kernelscale / bad prescale string) float_stride stays 0 -> zero- size mallocs -> OOB writes in picture_copy. Capture + check the return. - speed_chroma_cuda.c / speed_temporal_cuda.c: the init fail_pop label popped the CUDA context and returned -EIO WITHOUT free_cuda_buffers(_st) -> device + pinned-host buffers leak on partial cuMemAlloc/cuMemHostAlloc failure. Free before returning (mirrors fail_after_pop / free_all). Found by the full-fork correctness audit (PR4 + PR5). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…peed extractors speed_chroma_hip.c / speed_temporal_hip.c init: a post-alloc NULL check did a bare 'return -ENOMEM' that bypassed the free_cpu cleanup label, leaking up to 10/12 CPU scratch buffers. Route to free_cpu; move the label below #endif HAVE_HIPCC so the goto compiles in non-HIPCC builds. (eig-scratch +4 fix preserved.) Audit PR-4 (HIP twin). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CUDA SpEED chroma/temporal extractors computed the reference path correctly but the DISTORTED path wrong (~2x-high chroma scores). extract_channel / extract_fex_st saved the reference covariance and restored it before the distorted CPU-linalg, so the distorted linear-system solution AND eigenvalues used the REFERENCE covariance; the shared score kernel then used one eigenvalue array for both ref and dis entropy. The CPU reference (est_params) computes SEPARATE ref and dis covariance + eigenvalues. Confirmed by RTX-4090 instrumentation: ref var/entropy matched bit-exactly, dis var/entropy diverged (5.11 vs 3.54 / 211 vs 190). Temporal masked it (consecutive frames -> ref≈dis). Fix: drop the cov save/restore (distorted path keeps the distorted covariance); add a d_eigenvalues_ref device buffer + cuMemcpyDtoD the ref eigenvalues aside after the reference linalg; speed_score_kernel now takes ref_eigenvalues AND dis_eigenvalues (ref entropy uses ref, dis entropy uses dis). Applied to speed_chroma_cuda.c, speed_temporal_cuda.c, speed_score.cu. Verified on RTX 4090: test_cuda_speed_chroma_parity AND test_cuda_speed_temporal_parity now PASS bit-parity (<1e-4) vs CPU. Completes the GPU SpEED correctness fix (kernel-geometry + basis). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…asis) to HIP Ports the RTX-4090-verified CUDA fix to the HIP twins (speed_score.hip kernels + speed_chroma_hip.c + speed_temporal_hip.c): means kernel = global window per element (not per-tile); cov kernel = global submatrix sweep, scalar global means, divide by N once (not per-tile block-local sum); separate ref/dis eigenvalue basis (d_eigenvalues_ref + hipMemcpyDtoD stash; score kernel takes ref+dis eigenvalue arrays; drop the cov save/restore so dis keeps dis covariance). Same algorithm as the CUDA fix that passes bit-parity on the RTX 4090. Not runnable on this host (no AMD GPU) — CI / ROCm hardware compile-verifies. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…basis) to SYCL Ports the RTX-4090-verified CUDA fix to the SYCL twins (speed_chroma_sycl.cpp + speed_temporal_sycl.cpp): means kernel = one work-item per element, global window (not per-tile); cov kernel = global submatrix sweep with scalar global means, divide by N once (not per-tile block-local sum); separate ref/dis eigenvalue basis (d_eigenvalues_ref USM buffer + q.memcpy DtoD stash; score kernel takes ref+dis eigenvalue arrays; drop the cov save/restore). Preserves the existing solve-barrier 'active'-flag deadlock fix and the eig-scratch +4 sizing. Same algorithm as the CUDA fix that passes bit-parity on the RTX 4090. Not runnable on the fp64-less Arc here — CI / fp64 hardware compile+run-verifies. Completes the GPU SpEED correctness fix across all three backends. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lusoris
force-pushed
the
fix/speed-extractor-oob-deadlock-heap-corruption
branch
from
June 20, 2026 17:45
f4a51bc to
5952dec
Compare
lusoris
added a commit
that referenced
this pull request
Jun 20, 2026
Ship the deep-dive deliverables for the GPU SpEED correctness + safety PR: - changelog.d/fixed/gpu-speed-covariance-eigenbasis-correctness.md - docs/research/research-1120-gpu-speed-covariance-eigenbasis-correctness-2026-06-20.md (per-tile-vs-global covariance bug, shared-vs-separate ref/dis eigenbasis bug, co-located safety bugs, and the RTX-4090 bit-parity method) - docs/state.md: T-GPU-SPEED-COVARIANCE-EIGENBASIS-CORRECTNESS-2026-06-20 row under Recently closed - docs/rebase-notes.md: entry for fix/speed-extractor-oob-deadlock-heap-corruption - core/src/feature/cuda/AGENTS.md: rebase-sensitive invariant (global covariance + separate ref/dis eigenvalue bases) - docs/backends/cuda/overview.md: user-visible note that GPU SpEED scores now match CPU (numeric correction) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ship the deep-dive deliverables for the GPU SpEED correctness + safety PR: - changelog.d/fixed/gpu-speed-covariance-eigenbasis-correctness.md - docs/research/research-1120-gpu-speed-covariance-eigenbasis-correctness-2026-06-20.md (per-tile-vs-global covariance bug, shared-vs-separate ref/dis eigenbasis bug, co-located safety bugs, and the RTX-4090 bit-parity method) - docs/state.md: T-GPU-SPEED-COVARIANCE-EIGENBASIS-CORRECTNESS-2026-06-20 row under Recently closed - docs/rebase-notes.md: entry for fix/speed-extractor-oob-deadlock-heap-corruption - core/src/feature/cuda/AGENTS.md: rebase-sensitive invariant (global covariance + separate ref/dis eigenvalue bases) - docs/backends/cuda/overview.md: user-visible note that GPU SpEED scores now match CPU (numeric correction) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lusoris
force-pushed
the
fix/speed-extractor-oob-deadlock-heap-corruption
branch
from
June 20, 2026 17:47
5952dec to
bce7ee9
Compare
lusoris
marked this pull request as ready for review
June 20, 2026 17:59
lusoris
enabled auto-merge (squash)
June 20, 2026 17:59
The doc-substance gate (ADR-0167) requires a docs/metrics or docs/backends/sycl edit when core/src/feature/sycl/*.cpp changes. Add a "GPU backend parity" section to the speed_qa metric doc covering the speed_chroma / speed_temporal CUDA/HIP/SYCL correctness fix: global covariance window and separate ref/dis eigenvalue bases, with the RTX-4090 bit-parity verification pointer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5 of 7 tasks
This was referenced Sep 3, 2026
lusoris
added a commit
that referenced
this pull request
Sep 4, 2026
…ilure `speed_chroma_cuda.c` created `s->module` with `cuModuleLoadData()` and `s->stream` with `cuStreamCreate()`, both jumping to `fail_pop` on error. That label — and `fail_after_pop` — only popped the context and called `free_cuda_buffers()`, which releases device and host BUFFERS via `cuMemFree` / `cuMemFreeHost` and never touches the module or the stream. So every init failure after the module load leaked the module, and every failure after the stream create leaked both. This is a regression, not unfinished work. PR #1007 (86f5a02) added the `cuModuleUnload` / `cuStreamDestroy` calls inline to both labels; PR #1029 (15262c7), merged the same day as a descendant of #1007, deleted both blocks. The three sibling extractors named in the same docs/state.md row — `integer_ms_ssim_cuda.c`, `integer_psnr_hvs_cuda.c` and `ssimulacra2_cuda.c` — are correct on master, as is `speed_temporal_cuda.c`; `speed_chroma_cuda.c` was the only file left. Rather than re-inlining the calls into two labels that already drifted apart once, the release is factored into a single `release_cuda_module_and_stream()` helper called from both. It mirrors `close_fex_cuda()` exactly: stream first, then module, each NULL-guarded, with no context push — the labels have already popped, and `close_fex_cuda()` releases them the same way on the normal path. The helper also nulls both handles, so a later release cannot double-free. Found by an audit of the docs/state.md open-bug rows against origin/master, which narrowed this row from "four extractors" to one. Verified: the CUDA lane configures and builds clean (-Denable_cuda=true), with speed_chroma_cuda.c compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris
added a commit
that referenced
this pull request
Sep 4, 2026
…ilure `speed_chroma_cuda.c` created `s->module` with `cuModuleLoadData()` and `s->stream` with `cuStreamCreate()`, both jumping to `fail_pop` on error. That label — and `fail_after_pop` — only popped the context and called `free_cuda_buffers()`, which releases device and host BUFFERS via `cuMemFree` / `cuMemFreeHost` and never touches the module or the stream. So every init failure after the module load leaked the module, and every failure after the stream create leaked both. This is a regression, not unfinished work. PR #1007 (86f5a02) added the `cuModuleUnload` / `cuStreamDestroy` calls inline to both labels; PR #1029 (15262c7), merged the same day as a descendant of #1007, deleted both blocks. The three sibling extractors named in the same docs/state.md row — `integer_ms_ssim_cuda.c`, `integer_psnr_hvs_cuda.c` and `ssimulacra2_cuda.c` — are correct on master, as is `speed_temporal_cuda.c`; `speed_chroma_cuda.c` was the only file left. Rather than re-inlining the calls into two labels that already drifted apart once, the release is factored into a single `release_cuda_module_and_stream()` helper called from both. It mirrors `close_fex_cuda()` exactly: stream first, then module, each NULL-guarded, with no context push — the labels have already popped, and `close_fex_cuda()` releases them the same way on the normal path. The helper also nulls both handles, so a later release cannot double-free. Found by an audit of the docs/state.md open-bug rows against origin/master, which narrowed this row from "four extractors" to one. Verified: the CUDA lane configures and builds clean (-Denable_cuda=true), with speed_chroma_cuda.c compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris
added a commit
that referenced
this pull request
Sep 4, 2026
…ilure (#1260) `speed_chroma_cuda.c` created `s->module` with `cuModuleLoadData()` and `s->stream` with `cuStreamCreate()`, both jumping to `fail_pop` on error. That label — and `fail_after_pop` — only popped the context and called `free_cuda_buffers()`, which releases device and host BUFFERS via `cuMemFree` / `cuMemFreeHost` and never touches the module or the stream. So every init failure after the module load leaked the module, and every failure after the stream create leaked both. This is a regression, not unfinished work. PR #1007 (86f5a02) added the `cuModuleUnload` / `cuStreamDestroy` calls inline to both labels; PR #1029 (15262c7), merged the same day as a descendant of #1007, deleted both blocks. The three sibling extractors named in the same docs/state.md row — `integer_ms_ssim_cuda.c`, `integer_psnr_hvs_cuda.c` and `ssimulacra2_cuda.c` — are correct on master, as is `speed_temporal_cuda.c`; `speed_chroma_cuda.c` was the only file left. Rather than re-inlining the calls into two labels that already drifted apart once, the release is factored into a single `release_cuda_module_and_stream()` helper called from both. It mirrors `close_fex_cuda()` exactly: stream first, then module, each NULL-guarded, with no context push — the labels have already popped, and `close_fex_cuda()` releases them the same way on the normal path. The helper also nulls both handles, so a later release cannot double-free. Found by an audit of the docs/state.md open-bug rows against origin/master, which narrowed this row from "four extractors" to one. Verified: the CUDA lane configures and builds clean (-Denable_cuda=true), with speed_chroma_cuda.c compiled. Co-authored-by: Lusoris <lusoris@pm.me> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
5 of 9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the SpEED-extractor bug cluster from the full-fork correctness audit (the GPU backends were never run on real hardware — no GPU in CI). 9 commits, ~17 bug-instances across CPU + CUDA + HIP + SYCL.
Verified bit-parity on the RTX 4090 (
test_cuda_speed_chroma_parity+test_cuda_speed_temporal_parity): the GPU SpEED algorithm was ~7× wrong (per-tile vs global covariance) and chroma additionally ~2× wrong (shared ref/dis eigenvalue basis) — both fixed and verified.CRITICAL/HIGH bugs: CPU
speed.cheap-OOB (the CI all-backends SEGV); GPU eig-scratch heap corruption (n²+3n→n²+4n, all 6 GPU extractors); SYCL solve-barrier deadlock (DEVICE_LOST on Intel Arc); CUDA chroma+temporal host-reads-device-ptr SEGV; CUDA SpEED kernel-geometry + separate ref/dis basis (bit-parity verified); HIP + SYCL ports of the same; temporal-init unchecked return; CUDAfail_pop+ HIPfree_cpuinit leaks.Type
fix— bug fixcuda/sycl/hip— backend-specificChecklist
test_cuda_speed_chroma_parity+test_cuda_speed_temporal_parityPASS bit-parity).speed.c+ CUDA + HIP + SYCL all carry the fix).Bug-status hygiene (ADR-0165)
docs/state.mdupdated in this PR — rowT-GPU-SPEED-COVARIANCE-EIGENBASIS-CORRECTNESS-2026-06-20added under Recently closed.Netflix golden-data gate (ADR-0024)
assertAlmostEqual(...)score in the Netflix golden Python tests. (SpEED is not in the golden pairs; the golden gate is CPU-only and unaffected.)Deep-dive deliverables (ADR-0108)
docs/research/1120-gpu-speed-covariance-eigenbasis-correctness-2026-06-20.md(per-tile-vs-global covariance bug, shared-vs-separate ref/dis eigenbasis bug, co-located safety bugs, and the RTX-4090 bit-parity method).## Alternatives consideredsection (no separate ADR: these are correctness bug fixes, CLAUDE §12 r8 exempt).AGENTS.mdinvariant note — added tocore/src/feature/cuda/AGENTS.md(global covariancemeans[25]not per-tilemeans[25*num_blocks]; separate ref/dis eigenvalue bases; mirror any kernel-math change across CPU+CUDA+HIP+SYCL).changelog.d/fixed/gpu-speed-covariance-eigenbasis-correctness.md.docs/rebase-notes.mdunderfix/speed-extractor-oob-deadlock-heap-corruption (2026-06-20).Reproducer
Cross-backend numerical results
Known follow-ups
🤖 Generated with Claude Code