Skip to content

fix(sycl): correct libvmaf_sycl zero-copy VMAF on QSV-decoded 10-bit input - #1081

Merged
lusoris merged 3 commits into
VMAFx:masterfrom
Tualua:fix/sycl-qsv-zerocopy-p010-normalize
Aug 30, 2026
Merged

lusoris merged 3 commits into
VMAFx:masterfrom
Tualua:fix/sycl-qsv-zerocopy-p010-normalize

Conversation

@Tualua

@Tualua Tualua commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the libvmaf_sycl FFmpeg zero-copy filter returning VMAF score: nan (with integer_motion ~130× too high) on QSV-decoded 10-bit pairs on Intel Arc. A fresh investigation found two independent root causes — neither in the orchestration loop blamed by the prior FIX-01 attempt (which was verified incomplete): a shared QSV/VA surface pool (cross-decoder contamination) and raw P010 MSB-aligned pixels imported without the LSB normalization the CPU path gets for free. See ADR-1121.

Type

  • fix — bug fix
  • sycl / cuda / simd — backend-specific

Root cause & fix

  • BUG-1 (contamination). With one shared -hwaccel_device, FFmpeg gives both decoders one mfxSession/VA surface pool; their distinct mfxFrameSurface1 wrappers map to the same VASurfaceIDs, so HEVC ref[N±1] overwrites the surface AV1 wrote dis[N] into (sycl_dis[N]==sycl_ref[N±1]). Fix (FIX-03): require a separate -init_hw_device qsv=… per decoder — a documented usage contract (libvmaf can't see FFmpeg session topology from inside the filter).
  • BUG-2 (NaN + 64× motion). VA-API stores P010/P012 MSB-aligned (V_MSB = V_LSB << (16−bpc)); the CPU path gets >>6 via FFmpeg's P010LE→YUV420P10LE, the SYCL import did not. Fix (FIX-04): apply the >>(16−bpc) shift in the import — fused into the Tile4/Y-tiled de-tile store on the QSV/DG2 hot path (each sample shifted as written; no extra kernel, no second pass) and a standalone launch_p010_normalize() kernel only on the rare LINEAR/readback fallbacks (no-op for 8-bit).
  • No DMA-BUF coherency sync. A DMA_BUF_IOCTL_SYNC(READ) flush was tried and removed — proven insufficient for the contamination, and its SYNC_START is a blocking decoder-fence wait that serialised decode→compute and cut 4K throughput with zero correctness benefit. (vaSyncSurface() + the in-order queue already order decode→de-tile.)

Checklist

  • Commits follow Conventional Commits.
  • make format && make lint green locally (clang-tidy on touched SYCL TUs in-container).
  • If I touched any SIMD/GPU code path, I verified parity: SYCL↔CPU to 3 sig figs (table below).
  • If I touched a feature extractor with twins — N/A; this is VA-import plumbing, not a feature kernel; the kernels are unchanged.
  • New .c/.cpp/.h — none added; existing fork files edited (headers intact).
  • If this PR adds an ADR, the row lives in docs/adr/_index_fragments/1121-*.md + appended to _order.txt (README regenerated via concat-adr-index.sh).

Bug-status hygiene (ADR-0165)

  • docs/state.md updated — T-SYCL-ZEROCOPY-P010-NAN-2026-06-30 added to Recently closed.

Netflix golden-data gate

  • Did not modify any assertAlmostEqual(...). The bug is in the fork-added SYCL VA-import path; the CPU metric engine and golden pairs are untouched. (The fork D-03 oracle baseline was rebased to the de-contaminated value 97.2350 in .planning verification artifacts only — not Netflix golden data.)

Cross-backend numerical results

feature           cpu-oracle (sep-session)   sycl zero-copy (FIX-03+04)   delta
vmaf (500f)       97.2337                    97.2350                      0.0013
integer_motion2   26.6934 (max)              26.6934 (max)                identical
vmaf (30f)        97.5111                    97.5085                      0.0026
integer_motion2   1.15262 (max)              1.15262 (max)                identical
NaN count         0                          0                            —
determinism       —                          byte-identical across runs   (sha256 match)

Performance: the zero-copy path now defaults to direct SYCL dispatch — the
combined graph (ADR-0483) is a net throughput loss on VA-import (byte-identical
output, but the per-frame de-tile import + graph compute-barrier serialise
decode→compute, ~15–25% slower at 4K; ~33→42 fps on Arc A380). select_strategy
gains a va_import_path flag (after the env checks, so VMAF_SYCL_USE_GRAPH=1
still forces graph); users no longer need the deprecated VMAF_SYCL_NO_GRAPH=1.
Secondary: the MSB→LSB shift is fused into the de-tile kernel (no extra GPU pass)
and the DMA_BUF_IOCTL_SYNC blocking flush was removed. Default-dispatch output
is byte-identical to graph mode (sha256 match) — zero numeric change.

Deep-dive deliverables (ADR-0108)

  • Research digest — docs/research/2026-06-30-sycl-qsv-zerocopy-two-bug-investigation.md.
  • Decision matrix — ADR-1121 ## Alternatives considered (normalize-in-import vs in-kernel vs force-readback vs auto-detect-session vs ioctl-as-fix).
  • AGENTS.md invariant note — core/src/sycl/AGENTS.md: normalize on all import paths; FIX-02 is hardening not the fix; D-03 de-contaminated targets.
  • Reproducer / smoke-test command — below.
  • CHANGELOG fragment — changelog.d/fixed/1121-sycl-qsv-zerocopy-p010-normalization.md.
  • Rebase note — docs/rebase-notes.md → fix/sycl-qsv-zerocopy-p010-normalize (no ffmpeg-patch impact; invariants listed).

Reproducer

# Intel Arc A380, container localhost/vmafx-fix04 (or any -Denable_sycl=true build).
# Separate per-decoder QSV sessions (FIX-03) are mandatory.
ffmpeg -y -hide_banner \
  -init_hw_device drm=drm0:/dev/dri/renderD128 \
  -init_hw_device vaapi=va0@drm0 \
  -init_hw_device qsv=qsv_ref@va0 \
  -init_hw_device qsv=qsv_dis@va0 \
  -hwaccel qsv -hwaccel_output_format qsv -hwaccel_device qsv_ref -c:v hevc_qsv -i ref.mkv \
  -hwaccel qsv -hwaccel_output_format qsv -hwaccel_device qsv_dis -c:v av1_qsv  -i dis.mkv \
  -lavfi '[0:v][1:v]libvmaf_sycl=log_fmt=csv:log_path=out.csv' \
  -frames:v 500 -f null -
# Expect: VMAF ~97, integer_motion < 30, 0 NaN. (Old bug: VMAF ~34/nan, integer_motion ~3000+.)

Known follow-ups

  • The separate-session requirement is a documented contract, not enforced by the binary (libvmaf cannot observe FFmpeg's mfxSession topology). The libvmaf_sycl filter could log a one-time hint if it detects a shared VA pool — deferred.
  • ffmpeg-patch stack: no change — the fix is internal to the fork-added SYCL VA-import path (no public C-API / CLI / meson_options.txt / LIBVMAFContext delta); the separate-session pattern is an ffmpeg invocation, not a vf_libvmaf.c change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YCP4JCoduujJgccDRGvNKv

@Tualua
Tualua requested a review from lusoris as a code owner July 1, 2026 04:38
@lusoris lusoris closed this Aug 30, 2026
@lusoris lusoris reopened this Aug 30, 2026
lusoris added a commit that referenced this pull request Aug 30, 2026
…cker digest batching (#1127)

No PR merged on this repository between 2026-06-29 and 2026-08-30. master was
green on all 26 required checks the whole time; the blocker was a
self-reinforcing loop in the merge gate.

Required Checks Aggregator polls until every sibling workflow on the SHA reaches
a terminal state, and treats a sibling still queued/in_progress at the deadline
as a failure. The deadline was 90 minutes — itself a remediation after a
30-minute deadline "failed every PR" on 2026-05-09. A two-month Renovate backlog
then opened ~44 concurrent dependency PRs, the queue again outran the deadline,
and aggregators expired with no failing check (job 89636653774 ran 90m14s and
died on "Build — Ubuntu gcc (CPU) + DNN: in_progress; CodeQL: queued;
ShellCheck + shfmt: queued"). Because the aggregator is required, the expiry
blocks the merge, which keeps the queue deep, which causes the next expiry.

Two coordinated changes, one per half of the loop:

- Deadline 90 -> 240 minutes, timeout-minutes 100 -> 250. The job only polls the
  checks API, so a longer ceiling costs a runner slot rather than compute; the
  hosted-runner hard cap is 360 minutes.
- Batch Docker digest/pin refreshes into one Renovate PR. This was the only
  dependency class without a grouping rule, so ~11 base images each opened a
  separate PR and a separate full CI matrix run.

ADR-1123. Note the allocator returned 1121, which collides with contributor
PR #1081 — scripts/adr/next-free.sh queries origin only and cannot see ADR files
on contributor forks. 1121 was released and 1123 claimed by hand.

Co-authored-by: Lusoris <lusoris@pm.me>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
lusoris pushed a commit to Tualua/vmafx that referenced this pull request Aug 30, 2026
Netflix shipped VMAF v1 in libvmaf v3.2.0 (June 2026): still a libsvm
nu-SVR fusion, but VIF is removed and CAMBI (banding) + speed_chroma
(chroma) become core features; ADM/motion move to adm3/motion3. Six
built-in models for 1080p@3H / phone@5H / 4K@1.5H / 4K@3H (last on a
[0,110] range) + HFR variants; Netflix recommends 10-bit measurement
even for 8-bit SDR.

Adds a cited research digest and ADR-1122 deciding to adopt v1 as an
opt-in generation (v0.6.1 stays default), with the port scope: build in
the JSONs, fix SYCL cambi/speed_chroma (now core, currently failing on
Arc A380), handle the [0,110] range end-to-end, plumb CAMBI enc params,
and add new v1 golden refs in separate tests (v0.6.1 golden pairs
untouched). Aligns with the zero-copy 10-bit work (ADR-1121, PR VMAFx#1081).

Refs ADR-1122.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YCP4JCoduujJgccDRGvNKv
lusoris added a commit that referenced this pull request Aug 30, 2026
* docs(vmaf): research digest + ADR-1122 for VMAF v1 model port

Netflix shipped VMAF v1 in libvmaf v3.2.0 (June 2026): still a libsvm
nu-SVR fusion, but VIF is removed and CAMBI (banding) + speed_chroma
(chroma) become core features; ADM/motion move to adm3/motion3. Six
built-in models for 1080p@3H / phone@5H / 4K@1.5H / 4K@3H (last on a
[0,110] range) + HFR variants; Netflix recommends 10-bit measurement
even for 8-bit SDR.

Adds a cited research digest and ADR-1122 deciding to adopt v1 as an
opt-in generation (v0.6.1 stays default), with the port scope: build in
the JSONs, fix SYCL cambi/speed_chroma (now core, currently failing on
Arc A380), handle the [0,110] range end-to-end, plumb CAMBI enc params,
and add new v1 golden refs in separate tests (v0.6.1 golden pairs
untouched). Aligns with the zero-copy 10-bit work (ADR-1121, PR #1081).

Refs ADR-1122.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YCP4JCoduujJgccDRGvNKv

* docs(adr): satisfy markdownlint on the VMAF v1 research digest and ADR-1122

Formatting only — no content or conclusions changed.

- table delimiter rows to the repo's "compact" pipe style (MD060)
- `text` language on the feature_names fence (MD040)
- backtick the viewing-condition identifiers `1080p@3H` / `phone@5H` /
  `4K@1.5H` / `4K@3H`; MD034 read the dot in `4K@1.5H` as a TLD and flagged it
  as a bare URL. Backticks are also more correct — these are technical
  identifiers, not prose.
- angle-bracket the maintainer e-mail (MD034)

markdownlint-cli2 over the PR's changed files now reports 0 issues.

Note for anyone repeating this: `markdownlint-cli2 --fix` is unsafe here per
ADR-0864. It rewrote `4K@1.5H` to `<4K@1.5H>`, silently corrupting the text. The
autofix output was reverted and the fixes above applied by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Lusoris <lusoris@pm.me>
@lusoris
lusoris force-pushed the fix/sycl-qsv-zerocopy-p010-normalize branch from b502d0c to 6049056 Compare August 30, 2026 14:42
Tualua and others added 3 commits August 30, 2026 16:43
Document the fix for the libvmaf_sycl zero-copy NaN bug on QSV-decoded
10-bit pairs. Two root causes: a shared QSV/VA surface pool (cross-decoder
contamination) and a P010/P012 MSB/LSB pixel-range mismatch. Lands ahead
of the implementing commit per CLAUDE.md rule 8.

- ADR-1121 via _index_fragments + _order.txt (README regenerated by
  concat-adr-index.sh)
- research digest: two-bug investigation
- docs/backends/sycl/overview.md: separate-session decode contract, P010
  normalization, VMAF_SYCL_IMPORT_DEBUG env knob
- core/src/sycl/AGENTS.md: normalize-on-all-import-paths invariant
- changelog.d/fixed fragment; docs/rebase-notes.md entry
- docs/state.md: close T-SYCL-ZEROCOPY-P010-NAN-2026-06-30

Refs ADR-1121.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YCP4JCoduujJgccDRGvNKv
…input

libvmaf_sycl returned VMAF=nan with ~130x integer_motion on QSV-decoded
10-bit pairs. Root cause (ADR-1121) was two independent bugs, neither in
the orchestration loop:

- P010/P012 MSB/LSB mismatch (the NaN + 64x motion). VA-API delivers
  10/12-bit luma MSB-aligned (V_MSB = V_LSB << (16-bpc)); the CPU path
  gets >>6 via FFmpeg's P010LE->YUV420P10LE, the SYCL import did not.
  Apply the >>(16-bpc) shift in the import: fused into the Tile4 / Y-tiled
  de-tile store (each sample shifted as written -- no extra kernel, no
  second full-plane pass; the QSV/DG2 hot path), with a standalone
  launch_p010_normalize() kernel only on the rare LINEAR D2D / readback
  fallbacks (no per-sample store to fuse into). No-op for 8-bit NV12.

- Shared QSV/VA surface pool (the cross-frame contamination). Fixed by the
  separate-per-decoder-QSV-session invocation contract (documented in
  docs/backends/sycl/overview.md); libvmaf cannot observe FFmpeg session
  topology from inside the filter, so it is not enforced in code.

No DMA-BUF coherency sync: a DMA_BUF_IOCTL_SYNC(READ) flush was tried and
removed -- proven insufficient for the contamination, and its SYNC_START
is a blocking decoder-fence wait that serialised decode->compute and cut
4K throughput with zero correctness benefit.

Defaults the zero-copy path to direct SYCL dispatch instead of the
combined graph. The graph (ADR-0483) is a net throughput loss on the
VA-import path -- byte-identical output, but the per-frame de-tile import
plus the graph compute-barrier serialise decode->compute, ~15-25% slower
at 4K. vmaf_sycl_select_strategy() gains a va_import_path flag (passed
state->has_imported), returning DIRECT after the env-override checks so an
explicit VMAF_SYCL_USE_GRAPH=1 / VMAF_SYCL_DISPATCH=...:graph still wins.
Users no longer need the deprecated VMAF_SYCL_NO_GRAPH=1.

This change carries the supporting SYCL plumbing: the double-buffer slot
toggle + upload-slot getters, and the env-gated VMAF_SYCL_CHECKSUM D2H
probe + VMAF_SYCL_IMPORT_DEBUG logging used to diagnose the bug (kept as
permanent diagnostics; resolved once at init via the
vmaf_sycl_import_debug_enabled() accessor). Pre-existing clang-tidy debt
in the touched dmabuf_import.cpp is cleaned (const-correctness on two
Level Zero queue pointers + ze_result_t) per ADR-0141.

Verified on Intel Arc A380 (Shadow's Edge 4K HEVC->AV1): 0 NaN, SYCL VMAF
97.2350 vs CPU oracle 97.2337 over 500 frames, integer_motion2 max 26.69,
and byte-identical across repeated runs (deterministic). Netflix golden
assertions untouched.

Refs ADR-1121.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YCP4JCoduujJgccDRGvNKv
Maintainer fixup on the contributor's branch so the two failing gates pass.

Pre-Commit (Formatters + Basic Checks) was red because the branch's SYCL
sources had 19 clang-format violations against the repo's .clang-format --
verified by running clang-format --dry-run --Werror over the three files as
they stand in 83af211. Reformatted; the hunks are pure reflow (argument
packing and a ternary joined onto one line), no semantic change.

Markdown lint was red for four separate rules:

  - Both SYCL research digests get the house
    '<!-- markdownlint-disable MD013 MD060 -->' directive. 95 of the 544 docs
    under docs/research/ already carry exactly that pair; digests embed
    fixed-width tool output and evidence tables that 80 columns and the
    compact table style cannot represent.
  - sycl-zero-copy-nan-diagnosis.md: tag the sample-rows fence as 'text'
    (MD040) and promote the bold 'hyp 1' line under '## Verdict' to a real
    '###' heading (MD036).
  - ADR-1121: wrap the Deciders email in angle brackets (MD034).

No prose was reflowed and no content changed. ADR-0864 warns that
'markdownlint --fix' rewrites content in this tree, so every markdown edit
here is by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lusoris
lusoris force-pushed the fix/sycl-qsv-zerocopy-p010-normalize branch from 6049056 to ce95142 Compare August 30, 2026 14:44
@lusoris
lusoris merged commit 6d390a9 into VMAFx:master Aug 30, 2026
@lusoris lusoris added this to the 1.0.0 — First release milestone Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants