Repository navigation
fix(gpu): let the backend flush own GPU extractors under --threads (ADR-1197) - #1343
Merged
Merged
Conversation
…DR-1197)
`vmaf --threads N` failed on every GPU backend, for every N including 1, on
every input:
libvmaf ERROR context could not be synchronized
exit 234
The message is wrong, and that is worth stating first. Instrumenting
flush_context_cuda() shows all four driver calls returning success --
cuCtxPushCurrent=0 cuStreamSynchronize=0 cuCtxSynchronize=0 cuCtxPopCurrent=0
-- while the accumulated `err` is -22. The context was healthy throughout. A
feature extractor's return value shared a variable with the driver's result and
was reported under the driver's name.
Root cause: flush_context_threaded()'s first loop flushed every TEMPORAL
extractor, GPU ones included, while its second loop already skipped
VMAF_FEATURE_EXTRACTOR_CUDA deliberately. That asymmetry is the bug. Flushing a
temporal GPU extractor there ran its tail-batch drain BEFORE the pending
boundary collect in flush_context_cuda(), so the later collect re-emitted an
already-written index and the collector returned -EINVAL.
The duplicate write was not the only consequence, which is why the obvious fix
is wrong. Draining the tail before the boundary collect also emits the last
batch-boundary frame without the min() against the following frame that motion2
and motion3 are defined by. I built the narrow fix -- extend the existing guard
to cover the collect as well as the flush -- and measured it: --threads then
returned 82.823778 against serial 82.814059, with frame 39 of the Netflix pair
reading 4.382255 instead of 3.724278. CPU serial, CPU threaded and CUDA serial
all agree on the correct value. That fix trades a crash for a silently wrong
score, so it was rejected on evidence.
Instead the threaded flush no longer touches GPU extractors at all, and
flush_context_cuda / flush_context_sycl own them in both modes, running
collect-then-flush in the same order the serial path uses. The thread-pool
special case in flush_context_cuda is deleted rather than moved. SYCL had the
same defect and never had a guard in any form.
flush_context_cuda now keeps the extractor result and the driver result in
separate variables and reports them separately, so "context could not be
synchronized" means what it says.
flush_non_temporal_cpu_extractors() is split out of flush_context_threaded to
keep it inside the ADR-0141 function-size budget after the added condition;
clang-tidy reports 0 findings on libvmaf.c, unchanged from master.
Verified bit-identical to serial at N = 1, 2, 4 and 8, per-frame across all 48
frames and every metric key, on CUDA and on SYCL. The new
core/tools/test/test_vmaf_gpu_threads.sh asserts equality rather than exit
status -- checking only the exit code would have passed the wrong-score variant
-- and is registered for each GPU backend that is compiled in. It fails with
exit 234 on master and passes with this change.
meson test --suite=fast: 139 ok, 4 failed. Those 4
(test_cuda_float_adm_parity, test_cuda_psnr_hvs_parity,
test_cuda_ssimulacra2_parity, test_vmaf_cuda_gpumask) fail identically on
unmodified origin/master, verified by reverting only libvmaf.c in the same
build tree.
testdata/bench_all.sh hard-codes --threads 1, so its GPU rows were masked
failures for as long as this existed; noted as a follow-up in the state.md row.
no digest needed: trivial
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ate) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris
marked this pull request as ready for review
September 6, 2026 00:40
14 of 26 tasks
lusoris
added a commit
that referenced
this pull request
Sep 6, 2026
T-GPU-MOTION-FLUSH-DOUBLE-EMIT-2026-09-06 has the right root cause -- the tail-batch drain and the pending collect emitting the same index, whose duplicate-write -EINVAL surfaced as a context-sync error -- and PR #1343 (ADR-1197) fixed it on master. Its trigger was mis-stated. The row claimed any clip long enough to leave a partial motion batch, and that no GPU backend completes a scored run. Not reproducible: on the pre-fix build cd52f26, --backend cuda with --frame_cnt 13, 17, 45 and 47 all exit 0. The trigger is --threads on an EOF-terminated read, which this epic's own harness hard-codes to 1; pre-fix --threads 1 with no --frame_cnt exits 234, post-fix it exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
19 tasks done
lusoris
added a commit
that referenced
this pull request
Sep 6, 2026
T-GPU-MOTION-FLUSH-DOUBLE-EMIT-2026-09-06 has the right root cause -- the tail-batch drain and the pending collect emitting the same index, whose duplicate-write -EINVAL surfaced as a context-sync error -- and PR #1343 (ADR-1197) fixed it on master. Its trigger was mis-stated. The row claimed any clip long enough to leave a partial motion batch, and that no GPU backend completes a scored run. Not reproducible: on the pre-fix build cd52f26, --backend cuda with --frame_cnt 13, 17, 45 and 47 all exit 0. The trigger is --threads on an EOF-terminated read, which this epic's own harness hard-codes to 1; pre-fix --threads 1 with no --frame_cnt exits 234, post-fix it exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris
added a commit
that referenced
this pull request
Sep 6, 2026
…aselines (ADR-1185) (#1335) * feat(bench): per-backend performance baseline harness and refreshed baselines (ADR-1185) Refresh the per-backend throughput table, which had drifted: its rows were captured against model/vmaf_v0.6.1.json before vmaf_v1.0.16_3d0h became the default model (ADR-1169), and nothing recorded what a caller who passes no --model actually pays. Adds testdata/bench_backends.py, the repetition-and-median companion to testdata/bench_all.sh. It reports the median of N timed runs (default 3, after a discarded warmup) with min/max spread and the 1-minute load average sampled around every cell, engages exactly one backend per run through the exclusive --backend selector, and records the frames[0].metrics key count as a backend-engagement check. bench_all.sh is left alone because its stdout shape is consumed by the MCP run_benchmark tool (ADR-0517). Two defects surfaced while measuring and are filed in docs/state.md rather than fixed here, so a dedicated PR can carry the cross-backend parity gate: - T-GPU-MOTION-FLUSH-DOUBLE-EMIT-2026-09-06: on cd52f26 no GPU backend completes a scored run on a clip longer than one motion batch. CUDA, SYCL and HIP all abort with "problem flushing context" because the tail-batch re-emit in the motion twins is not idempotent while the feature collector rejects duplicate writes. CI cannot see it: the only lane building all three backends together runs on a GPU-less runner. - T-CUDA-MOTION-PARITY-576P-1.5E-2-2026-09-06: with CUDA unblocked, the 576x324 golden pair pools 1.50e-2 away from CPU, against the 6-dp match the same table recorded at commit 4130149. The published CUDA rows were measured with that flush defect patched locally and are labelled as not reproducible from master; SYCL and HIP rows are BLOCKED. CPU rows are master behaviour. Headline for retrain planning: the default model costs 1.6x the CPU time of v0.6.1 on the 576x324 golden pair, and on CUDA it gives up the entire GPU speedup - 4K throughput falls from 167.16 to 9.00 fps, below the CPU's own 17.52 fps, with the process at ~97% CPU and the 4090 at ~34% utilisation (ADR-1183 twin gating dispatching unsupported twins back to the host). Also corrects two stale claims that the measurement disproved: the docs/benchmarks.md reproduce block named a source root that stopped existing at ADR-0700, and core/AGENTS.md asserted that --backend cuda initialises CUDA but scores on the CPU, which is no longer true. No Netflix golden assertion touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(bench): record the pristine-master cross-check for the CPU baseline rows Re-running the 576x324 CPU cells against an unpatched all-backends build of the same commit reproduces the pooled scores exactly (76.667831 / 82.816062), confirming the published CPU rows are master behaviour and not an artefact of the locally-patched build the CUDA rows needed. Throughput landed at 551.24 / 349.46 fps against the published 613.71 / 379.15 at roughly twice the load, which is recorded as the honest scale of session-to-session movement on this host. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(state): close the double-emit row and correct its trigger T-GPU-MOTION-FLUSH-DOUBLE-EMIT-2026-09-06 has the right root cause -- the tail-batch drain and the pending collect emitting the same index, whose duplicate-write -EINVAL surfaced as a context-sync error -- and PR #1343 (ADR-1197) fixed it on master. Its trigger was mis-stated. The row claimed any clip long enough to leave a partial motion batch, and that no GPU backend completes a scored run. Not reproducible: on the pre-fix build cd52f26, --backend cuda with --frame_cnt 13, 17, 45 and 47 all exit 0. The trigger is --threads on an EOF-terminated read, which this epic's own harness hard-codes to 1; pre-fix --threads 1 with no --frame_cnt exits 234, post-fix it exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(state): drop the duplicate rows a keep-both rebase created Each dropped row restates one origin/master already carries; master is the authoritative record. Verified with scripts/ci/check-state-md-rows.sh. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Lusoris <lusoris@pm.me> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
3 of 6 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
vmaf --threads Nfailed on every GPU backend, for everyNincluding 1, on every input:The message is wrong, and that is the first thing worth stating. Instrumenting
flush_context_cuda()shows all four driver calls returning success —cuCtxPushCurrent=0 cuStreamSynchronize=0 cuCtxSynchronize=0 cuCtxPopCurrent=0— while the accumulatederris-22. The context was healthy throughout; a feature extractor's return value shared a variable with the driver's and got reported under the driver's name.Root cause:
flush_context_threaded()'s first loop flushed everyTEMPORALextractor, GPU ones included, while its second loop already skippedVMAF_FEATURE_EXTRACTOR_CUDAdeliberately. That asymmetry is the bug.testdata/bench_all.shhard-codes--threads 1, so its GPU rows were masked failures for as long as this existed.Why the obvious fix is wrong (measured, not argued)
Draining the tail before the boundary collect does not only cause a duplicate write. It also emits the last batch-boundary frame without the
min()against the following frame thatmotion2/motion3are defined by. I built the narrow fix — extend the existing guard to cover the collect as well as the flush — and measured it:integer_motion2_mmxv_18--threads 4--threads 4, narrow fix--threads 4, this PREvery other feature was bit-identical; exactly one frame was wrong. The narrow fix trades a crash for a silently wrong score, which is strictly worse, so it was rejected on evidence.
What this does
flush_context_threaded()no longer touches GPU extractors at all — its first loop now skipsCUDA/SYCLflags, matching its second loop — andflush_context_cuda()/flush_context_sycl()own them in both modes, running collect-then-flush in the order the serial path uses. The thread-pool special case inflush_context_cuda()is deleted, not moved. SYCL had the same defect and never had a guard in any form.flush_context_cuda()also keeps the extractor result and the driver result in separate variables, so "context could not be synchronized" now means that.Type
fix— bug fixsycl/cuda/simd— backend-specificChecklist
make format && make lintis green locally —pre-commit run --filesclean on all touched files;clang-tidy -p build core/src/libvmaf.creports 0 findings, unchanged from master (the added condition pushedflush_context_threadedover the ADR-0141 size budget, soflush_non_temporal_cpu_extractors()was split out rather than suppressed with a NOLINT).meson test --suite=fast→ 139 ok, 4 failed. Those 4 (test_cuda_float_adm_parity,test_cuda_psnr_hvs_parity,test_cuda_ssimulacra2_parity,test_vmaf_cuda_gpumask) fail identically on unmodifiedorigin/master, verified by reverting onlylibvmaf.cin the same build tree and re-running them.N= 1, 2, 4, 8, compared per-frame across all 48 frames and every metric key, so the worst delta is 0 ULP. GPU-vs-CPU is unchanged by this PR and remains non-bit-exact by design..c/.cpp/.cu/.h/.hpp, it has the license header. — no new C source; the new file is a shell test.docs/adr/_index_fragments/and the slug is in_order.txt— number allocated withscripts/adr/next-free.sh --claim;README.mdregenerated and--checked.Bug-status hygiene (ADR-0165)
docs/state.md—T-GPU-CLI-THREADS-CTX-SYNC-2026-09-06added to Recently closed with the misreport evidence, both consequences, the rejected narrow fix and its measured wrong score, and thebench_all.shfollow-up.Netflix golden-data gate (ADR-0024)
assertAlmostEqual(...)score in the Netflix golden Python tests.Cross-backend numerical results
Deep-dive deliverables (ADR-0108)
## Alternatives consideredweighs five options, including the narrow guard extension that was built and measured before rejection.AGENTS.mdinvariant note — no rebase-sensitive invariants: the two that matter are recorded indocs/rebase-notes.md, which is where this repo keepscore/src/libvmaf.cones.changelog.d/fixed/adr-1197-gpu-threads-flush.md;CHANGELOG.mdregenerated and--checked.docs/rebase-notes.md,fix/gpu-threads-ctx-sync — threaded flush leaves GPU extractors alone (2026-09-06).Reproducer
The test asserts the threaded result equals the serial one per frame, not merely that the process exits 0 — checking only the exit code would have passed the wrong-score variant above.
Known follow-ups
testdata/bench_all.shGPU numbers need re-taking. It pins--threads 1, so every GPU row it recorded while this bug existed was a failure, not a measurement. Related: fix(testdata): make the Netflix benchmark harness honest and portable (ADR-1192) #1334, which re-ran that harness and surfaced the--threadsreproducer this PR fixes.libvmaf_cudareturning a non-modal score in ~10 of 40 runs. That is a distinct defect and is not addressed here; I verified separately that fix(cuda): scope the drain batch to its engine and fence the indexed reads (Netflix/vmaf#1305) #1321 does not fix it either.