Skip to content

fix(sycl): return from vmaf_sycl_upload_plane only after its copy has completed - #2102

Merged
lusoris merged 4 commits into
masterfrom
fix/sycl-upload-plane-fence
Oct 5, 2026
Merged

lusoris merged 4 commits into
masterfrom
fix/sycl-upload-plane-fence

Conversation

@lusoris

@lusoris lusoris commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Summary

vmaf_sycl_upload_plane() returned with its host-to-device copy still in flight. Nothing ordered that copy before the frame's compute, which runs on other queues: sycl_apply_input_barriers() waits on last_upload_event, which this function never set. The caller could also free the source while the copy was still reading it; the Windows D3D11 import unmaps its staging texture right after the call. On an Arc A380, every 3840x2160 frame uploaded this way and read at once scored the wrong pixels.

The function now orders the copy behind the slot's previous readers and waits for it before returning. This closes T-SYCL-UPLOAD-PLANE-NO-COMPUTE-FENCE-2026-10-05, which #2075 opened (now on master).

What reproduced (A380, xe)

test_sycl_zero_copy_model_gate, new case test_upload_plane_orders_compute: 8 frames of 3840x2160 that change every frame, uploaded with vmaf_sycl_upload_plane(), read at once by psnr_sycl (enable_chroma=false), compared with the CPU psnr. The test frees its source pictures right after the read, as a caller may.

Build Frames equal to the CPU (%.17g)
without the fix 0 of 8, on 3 of 3 runs (psnr_y 4.9 to 6.1 where the CPU gives 7.04 to 7.15)
copy event recorded for the compute barrier, no wait 6 of 8 wrong, on 3 of 3 runs
this PR (fence behind the slot's readers, then wait) 8 of 8, on 3 of 3 runs

The test's other cases (576x324) were right before the fix too, because their copies finish first. Their explicit vmaf_sycl_wait_copy_queue() is gone, so every case now runs without a wait of the caller's own.

What changed

  • core/src/sycl/common.cpp::vmaf_sycl_upload_plane(): sycl_fence_slot_readers() first (as vmaf_sycl_shared_frame_upload() does, ADR-1369), then last_ev.wait_and_throw() before returning.
  • common.cpp, touched, is brought from 4 to 0 sycl-lane clang-tidy findings. Its four getenv() calls (VMAF_SYCL_PROFILE, VMAF_SYCL_TIMING, VMAF_SYCL_IMPORT_DEBUG, VMAF_SYCL_CHECKSUM) now read through the thread-safe snapshot vmaf_gpu_dispatch_env_get() (ADR-0488). The scoped baseline was tightened in the dev container.
  • Docs: the libvmaf_sycl.h comment, docs/api/gpu.md and docs/backends/sycl/zero-copy.md state that the call returns after the copy.

Verification (A380; SYCL build with dg2-g11 AOT)

Check Result
test_sycl_zero_copy_model_gate 5 of 5, three runs
the 66 fast+gpu+sycl tests 66 of 66
test_vmaf_sycl_threads, test_vmaf_feature_backend_sycl (slow) pass
--suite fast without gpu 328 pass, 0 fail, 1 skipped
scripts/dev/tidy-lane.sh --write --only core/src/sycl/common.cpp sycl (dev container) exit 0; common.cpp 4 → 0, tightened; the test file stays at 0

Not verified: the Windows D3D11 path, which calls this function (no Windows GPU host).

Deep-dive deliverables (ADR-0108)

  • Research digest — no digest needed: one ordering fix, measured in the state row
  • Decision matrix — no alternatives: only-one-way fix (the source is the caller's memory, so the copy must finish before the call returns)
  • AGENTS.md invariant note — core/src/sycl/AGENTS.md ("vmaf_sycl_upload_plane() returns after its copy completed")
  • Reproducer / smoke-test command — below
  • CHANGELOG fragment — changelog.d/fixed/sycl-upload-plane-waits-for-copy.md
  • Rebase note — docs/rebase-notes.md, "vmaf_sycl_upload_plane waits for its copy (2026-10-05)"

Reproducer

source /opt/intel/oneapi/setvars.sh
CC=icx CXX=icpx meson setup build-sycl core -Denable_sycl=true -Dsycl_icpx_aot_targets=dg2-g11
ninja -C build-sycl test/test_sycl_zero_copy_model_gate
flock ~/.cache/vmafx-locks/sycl-a380.lock timeout 300 build-sycl/test/test_sycl_zero_copy_model_gate

@github-actions github-actions Bot added the type:bug Something isn't working label Oct 5, 2026
@lusoris
lusoris force-pushed the fix/sycl-upload-plane-fence branch from 3fafbee to 7f7c11a Compare October 5, 2026 20:08
xxxxxxxxxxxxx and others added 4 commits October 5, 2026 23:08
…t INT64_MAX scores (#2190)

* fix(adm): sum the scale-0 contrast-masking rows unsigned so a row past INT64_MAX scores

The integer ADM masking reduction adds, per row, the non-negative cubes of
the CSF-weighted coefficients above their threshold, and the CPU, its
AVX2 / AVX-512 rows and the CUDA, HIP and SYCL twins summed a scale-0 row
in int64. Compared with itself, a 31-32 or 63-64 pixel wide picture with
full-range reference detail (core/test/adm_cm_row_overflow_frame.h) takes
one row to 1.044 / 1.021 INT64_MAX at the default weights: the sum
wrapped, the numerator was NaN and the frame failed with "invalid ADM
reduction". A horizontal or vertical CSF weight above 38,400 does the
same at 16K. Scale 0 now sums its rows and its frame unsigned
(adm_cm_round_row_total_s0(), adm_cm_fold_s0(), uint64_t rows in every
backend; the GPU hosts read the scale-0 slots as uint64_t), which holds
every row at the default weights (0.52 of 2^64 at worst) and up to an h/v
weight of about 45,200. Below 2^63 the values keep their bits. Scales 1-3
keep their signed sums. The weights between 45,200 and the ADR-1472 limit
of 46,603 are left open as T-ADM-CM-SCALE0-ROW-UINT64-WEIGHT-BUDGET.

The CUDA and HIP scale 1-3 decouple also narrowed the gain product
(|o| up to 1.45e9 times the limit of 100) to int32 before bounding it by
t, an undefined conversion the devices' saturation hid; they now bound
the double product with fmin / fmax and narrow once, as the CPU does.

test_integer_adm_cm_row_unsigned (8, 10, 16 bit; SIMD == scalar) and the
new row case of test_gpu_adm_tiny_frames fail on master and pass on the
CPU, CUDA, HIP and SYCL; the fold and gain contracts pin both forms; the
Netflix golden gate passes.

Closes T-ADM-CM-SCALE0-ROW-INT64-OVERFLOW-2026-10-05.
Closes T-GPU-ADM-S123-GAIN-PRODUCT-NARROWING-2026-10-05.
…filters after a mid-run error (#2115)

* fix(ffmpeg): print no pooled score from the libvmaf and libvmaf_cuda filters after a mid-run error

Upstream FFmpeg's libvmaf and libvmaf_cuda filters share one uninit().
After a failed picture copy or vmaf_read_pictures(), the run exited
non-zero, but uninit() still pooled the frames read before the error and
printed a VMAF score line. frame_cnt had already counted the failed
frame, so the pooling usually failed and the line showed an uninitialised
value: "VMAF score: 0.000000" for both filters in the dev container. The
error line named neither the frame nor the error.

As the maintainer decided, new series patch 0021 makes these filters
follow the rule libvmaf_sycl and libvmaf_metal follow since ADR-1761
(ADR-1768):
- one error names the filter, the step, the frame and the error;
- the frame is freed and the filter records the stop;
- frame_cnt advances only after a successful read;
- uninit() prints no pooled score and writes no report after a stop, a
  failed flush or a failed pooled score.
The divergence from upstream is deliberate and is kept on every series
refresh.

ffmpeg-patches/test/check-libvmaf-no-score-after-error.sh injects the
errors through the LD_PRELOAD shim. The shim is renamed
fault_inject_libvmaf.c and now injects vmaf_read_pictures() failures too.
The script runs the CPU filter, and libvmaf_cuda on the RTX 4090. With
this branch's series, 17 of 17 checks pass; the previous FFmpeg fails 6.
core/test/test_ffmpeg_libvmaf_stop_contract.py checks the series without
a device.

* docs: regenerate the indexes and the citation map after rebasing
* ci(workflows): do not cancel master push runs by concurrency

A push to master no longer cancels the runs of the previous master commit.
The concurrency group of every push-to-master workflow carries github.sha on
master and cancel-in-progress stays on for pull request refs. Publish,
release-please, Scorecard and the Pages deploy job stay serialised and are
listed with reasons in a new contract test. The Required Checks Aggregator
takes the same form, so re-running an old master head cannot cancel the
newest one.

* ci(workflows): type the master concurrency contract test

* test(ffmpeg): update workflow concurrency group expectation for master push

* docs: regenerate the indexes and the citation map after rebasing
… completed (#2102)

* fix(sycl): return from vmaf_sycl_upload_plane only after its copy has completed

vmaf_sycl_upload_plane() enqueued its host-to-device copy on the copy
queue and returned. Nothing ordered that copy before the frame's compute
on the other queues: sycl_apply_input_barriers() waits on
last_upload_event, which this function never set. The caller was also
free to release the source while the copy still read it; the Windows
D3D11 import unmaps its staging texture right after the call.

On an Arc A380, eight 3840x2160 frames uploaded this way and read at
once with psnr_sycl were all wrong (psnr_y 4.9 to 6.1 where the CPU gives
about 7.1), on three of three runs. Recording the copy's event for the
compute barrier, without waiting, still left six of eight wrong.

The function now orders the copy behind the slot's previous readers, as
vmaf_sycl_shared_frame_upload() does (ADR-1369), and waits for the copy
before returning. All eight frames now equal the CPU on three of three
runs (test_sycl_zero_copy_model_gate, test_upload_plane_orders_compute).
The test's other cases no longer wait on the copy queue themselves.

common.cpp, touched, is brought to zero clang-tidy findings: its four
getenv() calls read through the thread-safe snapshot helper
vmaf_gpu_dispatch_env_get() (ADR-0488).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants