Skip to content

feat(motion): compute motion_five_frame_window on the CUDA, SYCL and HIP twins (ADR-1491) - #1893

Merged
lusoris merged 2 commits into
masterfrom
port/motion-five-frame-window-gpu-twins
Oct 3, 2026
Merged

lusoris merged 2 commits into
masterfrom
port/motion-five-frame-window-gpu-twins

Conversation

@lusoris

@lusoris lusoris commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The CUDA, SYCL and HIP twins of motion and motion_v2 now compute Netflix's motion_five_frame_window, and every output equals the CPU's bit for bit. Follows #1887 (the CPU port, ADR-1478, on master as 6d857a402), which has these twins hand the option to the CPU extractor. A GPU run of a vmaf_v1.0.16_hfr_* model keeps its motion feature on the device.

ADR-1491 records the design. Maintainer decision (popup, 2026-10-02): "Port now, CPU and twins (Recommended)".

Base: master (restacked onto 6d857a402 after #1887 landed; one commit).

What changed

No kernel changes on any backend: the SAD kernels already take the two planes they difference as arguments.

Twin The frame two back motion2 / motion3
motion_cuda ring of three raw planes (raw[n % 3] written, raw[(n + 1) % 3] read); the previous frame's event is waited for from frame 1 on, so the chain reaches the copy of frame n-2 with the option: SAD score per frame in collect(), the CPU's vmaf_motion_window_flush() in flush(); three-frame path unchanged
motion_v2_cuda ring of three, as above vmaf_motion_window_flush() for both windows; its copy of the CPU flush is gone
motion_hip two kept planes; frame n reads plane n % 2, the copy behind the SAD overwrites it as motion_cuda
motion_v2_hip two kept planes as motion_v2_cuda
motion_sycl two planes with fixed roles (n-2, n-1), advanced by two device copies behind the graph replay; the kernel runs on every frame, because the combined command graph is recorded once and replayed as motion_cuda
motion_v2_sycl ring of three as motion_v2_cuda
motion_metal, motion_v2_metal not changed: they do not declare the option, so the CPU extractor computes it (not run, no device)
  • Option rows are the CPU's on all six twins (no VMAF_OPT_FLAG_DEFAULT_ONLY, no -ENOTSUP). motion_sycl refuses the option together with its own motion_add_uv (-ENOTSUP): the CPU motion has no chroma mode to equal.
  • With the option the motion twins publish motion2 and motion3 at the end of the run, as the CPU does, not frame by frame.
  • Parity gate: two cells, motion_mffw and motion_v2_mffw (the option with the moving average, the option set of the HFR models), declared exact for CUDA, SYCL and HIP in scripts/ci/exact_twins.d/.
  • Tests: core/test/motion_five_frame_twin_parity.h (fixture, six option sets, comparison) and test_{cuda,sycl,hip}_motion_five_frame_window; option-table rows in the three test_*_twin_option_parity; test_gpu_option_value_capability_contract.py now holds the six twins to full-range options; test_motion_five_frame_window checks the dispatch verdict of every registered twin. The device-free source contracts test_{cuda,sycl,hip}_kernel_source_contract.py pinned the text of the motion_v2 twins' own flush (no re-weighting, the one-frame case); they now pin that each twin calls vmaf_motion_window_flush() and reads no stored score back, with planted regressions for both.

Verification

== on every output unless stated otherwise.

Check RTX 4090 (CUDA) Arc A380, xe (SYCL) gfx1036 (HIP)
test_<backend>_motion_five_frame_window: both extractors, six option sets (alone; moving average + debug; every motion option; motion_force_zero; motion_v2 alone and with every option), 11 / 1 / 2 / 3 frames, 8 and 10 bits pass pass, also with VMAF_SYCL_USE_GRAPH=1 and VMAF_SYCL_NO_GRAPH=1; and the add_uv refusal pass
Clips at --precision max: Netflix 576x324 (8 and 10 bit), 1080p checkerboard, BBB 3840x2160 50 frames; three --feature option sets and vmaf_v1.0.16_hfr_3d0h 1352 of 1352 motion values identical 1352 of 1352 1352 of 1352
Parity gate motion_mffw, motion_v2_mffw (exact) on the Netflix pair (48 frames) and BBB 4K (200 frames) 0 / 0 0 / 0 (graph replay at 4K, direct at 576x324; both modes also forced) 0 / 0
Gate cells motion, motion_debug, motion_v2 (exact), same clips 0 0 0
Existing motion tests: *_exact_twins, *_motion3_parity, *_motion_sad_score, *_motion_v2_parity, *_motion_tiny_frames, *_twin_option_parity (+ test_hip_upload_race, test_sycl_motion_add_uv_parity, test_sycl_kernel_scratch) pass pass pass
  • Mutation check: with the CUDA twins taking the SAD against frame n-1 instead of n-2, test_cuda_motion_five_frame_window fails on 112 outputs.
  • sycl-aot suite (every SYCL translation unit for the 19 default targets): see "SYCL ahead-of-time" below.
  • scripts/ci/test_cross_backend_parity_gate.py: 91 passed; core/test/test_parity_gate_metric_names.py, test_gpu_option_value_capability_contract.py: pass.
  • Whole suites on this head (381a1e3ba, restacked onto master 6d857a402), every device test alone under its device lock: CUDA + HIP build (GCC, nvcc, hipcc) 290 device-free and 140 device tests pass; SYCL build (icx) 272 device-free and 67 device tests pass, 1 skipped (test_cuda_parity_gate_default_run, a build without CUDA). The gate cells motion, motion_debug, motion_v2, motion_mffw and motion_v2_mffw were measured again on this head: 0 on all three backends, both clips.

SYCL ahead-of-time: VMAF_SYCL_AOT_JOBS=8 meson test -C build-sycl --suite sycl-aot: test_sycl_aot_default_targets OK on this head (35 SYCL translation units for the 19 default targets, 150 s); the restack brought master's SYCL changes (integer_adm_sycl.cpp, integer_psnr_hvs_sycl.cpp, sycl_exact_fp.h). No kernel is added or changed: the scratch-memory ratchet (test_sycl_kernel_scratch, pass) and the sub-group sizes are those of master.

Type

  • feat — new feature
  • fix — bug fix
  • perf — performance improvement
  • refactor — no behavior change
  • docs — documentation only
  • test — test-only
  • build / ci — tooling / infra
  • port — cherry-pick from upstream Netflix/vmaf
  • sycl / cuda / simd — backend-specific

Checklist

  • Commits follow Conventional Commits (the commit-msg hook enforces this).
  • make format && make lint is green locally. (clang-format, black, ruff, markdownlint through the commit hooks; praetorctl audit on the touched files: clean after splitting enqueue_motion_work().)
  • Unit tests pass: python3 scripts/ci/run_meson_test.py -- -C build. (Device tests listed under "Verification".)
  • If I touched any SIMD/GPU code path, I ran /cross-backend-diff and the worst ULP is ≤ 2. (Parity gate on all three backends, exact cells: 0.)
  • If I touched a feature extractor with SIMD/GPU twins, I either updated every twin or listed the gap under "Known follow-ups" below. (Metal.)
  • If I added a new .c / .cpp / .cu / .h / .hpp, it has the appropriate license header. (Three tests and one test header, EUPL-1.2.)
  • If this is a breaking change, the commit message uses ! or BREAKING CHANGE: and the migration path is documented below. (Not breaking: twins gain an option the CPU has.)
  • If this PR adds an ADR, the ADR row lives in docs/adr/_index_fragments/<NNNN-slug>.md and the slug is appended to docs/adr/_index_fragments/_order.txt.

Bug-status hygiene (ADR-0165)

  • docs/state.md updated in this PR: T-GPU-MOTION-FIVE-FRAME-WINDOW-2026-10-02 moved from "Open bugs" to "Recently closed" with the measurements.

Netflix golden-data gate (ADR-0024)

  • I did not modify any assertAlmostEqual(...) score in the Netflix golden Python tests. (No Python test and no CPU extractor changes here.)
  • If I believe a golden value must change, I have explained why below AND pinged @lusoris for a CODEOWNERS exception. (None changes.)

Cross-backend numerical results

motion_mffw     cpu-vs-cuda 0  cpu-vs-sycl 0  cpu-vs-hip 0   (Netflix 48 frames, BBB 4K 200 frames)
motion_v2_mffw  cpu-vs-cuda 0  cpu-vs-sycl 0  cpu-vs-hip 0

Deep-dive deliverables (ADR-0108)

  • Research digest — no digest needed: the measurements are in ADR-1491 and the state row.
  • Decision matrix — ADR-1491 ## Alternatives considered.
  • AGENTS.md invariant note — core/src/feature/cuda/AGENTS.d/motion-five-frame-window.md, core/src/feature/hip/AGENTS.d/motion-five-frame-window.md, core/src/feature/sycl/AGENTS.d/motion-five-frame-window.md, and the pages that described the old behaviour; index entry in docs/development/rebase-sensitive-invariants.md.
  • Reproducer / smoke-test command — pasted below under "Reproducer".
  • CHANGELOG fragment — changelog.d/changed/gpu-motion-five-frame-window.md.
  • Rebase note — docs/rebase-notes.md, "The motion twins compute motion_five_frame_window (ADR-1491, 2026-10-02)".

Reproducer

# On master without this PR the twin is not used: the receipt reads {"extractor": "motion", "backend": "cpu"}.
core/build/tools/vmaf --backend cuda \
    --reference python/test/resource/yuv/src01_hrc00_576x324.yuv \
    --distorted python/test/resource/yuv/src01_hrc01_576x324.yuv \
    --width 576 --height 324 --pixel_format 420 --bitdepth 8 \
    --model version=vmaf_v1.0.16_hfr_3d0h --precision max --json --output /dev/stdout

python3 scripts/ci/cross_backend_parity_gate.py --vmaf-binary core/build/tools/vmaf \
    --reference testdata/ref_576x324_48f.yuv --distorted testdata/dis_576x324_48f.yuv \
    --width 576 --height 324 --backends cpu cuda --features motion_mffw motion_v2_mffw

Known follow-ups

  • Metal: the twins do not declare the option and the CPU extractor computes it. Not run: no device.
  • With the option, motion_sycl copies one more luma plane per frame (two device copies instead of one). No performance claim here; measuring it belongs to RC8 (candidate map of ADR-1490).

@github-actions github-actions Bot added the type:feature New feature or request label Oct 2, 2026
@lusoris
lusoris force-pushed the port/upstream-motion-five-frame-window branch from d8e4985 to 31009e9 Compare October 2, 2026 22:21
@lusoris
lusoris force-pushed the port/motion-five-frame-window-gpu-twins branch from 3dce37b to de52f4b Compare October 2, 2026 22:29
@lusoris
lusoris force-pushed the port/upstream-motion-five-frame-window branch 2 times, most recently from 2564d32 to 6d857a4 Compare October 3, 2026 00:10
Base automatically changed from port/upstream-motion-five-frame-window to master October 3, 2026 00:10
@lusoris
lusoris force-pushed the port/motion-five-frame-window-gpu-twins branch from de52f4b to 381a1e3 Compare October 3, 2026 00:27
…e the function-size limit (#1898)

* test(ssim): split the 8x8 float_ssim extractor test so it stays inside the function-size limit

test_float_ssim_extractor_8x8_is_zero() (fork #1882) trips
readability-function-size: every mu_assert() is a branch. Since #1882
the Tidy Ratchet lanes count one finding above their baselines on
master (hip 505 against 504, arm64 116 against 115, and the required
cpu lane).

The context setup moves into open_float_ssim_8x8() and one frame of the
run into extract_8x8_frame(); the test keeps the loop and the teardown.
The return values the test dropped (vmaf_picture_unref(), the context's
close and destroy) are now checked. The test asserts the same values and
passes; no baseline is edited. With the split, scripts/dev/tidy-lane.sh
measures cpu 70, cuda 620, hip 504 and arm64 115 warnings, each its
baseline.
…HIP twins (ADR-1491) (#1893)

* feat(motion): compute motion_five_frame_window on the CUDA, SYCL and HIP twins (ADR-1491)

The CPU extractors motion and motion_v2 have Netflix's five-frame window
since ADR-1478; their GPU twins handed the option to the CPU. The CUDA,
SYCL and HIP twins of both extractors compute it now, and every output
equals the CPU's bit for bit on an RTX 4090, an Arc A380 and a gfx1036.

No kernel changes: the SAD kernels take the two planes they difference as
arguments, and with the option one of them is the frame two back.

- CUDA and motion_v2_sycl keep a ring of three raw planes instead of two.
  The CUDA twins wait on the previous frame's event from frame 1 on, so
  the chain of events reaches the copy of the frame two back.
- HIP keeps two planes instead of one; frame n reads plane n % 2 and the
  copy behind the SAD overwrites it.
- motion_sycl gives its two planes fixed roles (frame n-2, frame n-1) and
  advances them with two device copies behind the graph replay. The kernel
  is enqueued on every frame: the combined command graph is recorded once
  and replayed, so a frame's work cannot depend on its index.
- motion2 and motion3 come from the CPU's own vmaf_motion_window_flush().
  With the option the motion twins store the SAD score per frame and run
  the window at the flush; their three-frame path is unchanged. The
  motion_v2 twins use the function for both windows and lose their copies
  of the CPU flush.
- The six twins carry the CPU's option row. motion_sycl refuses the option
  together with its own motion_add_uv, which the CPU has no mode for.
- The Metal twins are unchanged: they do not declare the option, so the CPU
  extractor computes it there. Not run, no device.

The parity gate has two exact cells for it, motion_mffw and
motion_v2_mffw: 0 on all three backends on the Netflix pair and on 200
frames of BBB 3840x2160. test_{cuda,sycl,hip}_motion_five_frame_window
compare every output of both extractors with == for six option sets, 11,
1, 2 and 3 frames, at 8 and 10 bits; a twin that takes the SAD against the
previous frame fails it on 112 outputs.

* docs: regenerate the indexes and the citation map after rebasing
@lusoris
lusoris force-pushed the port/motion-five-frame-window-gpu-twins branch from 381a1e3 to 94ef3fd Compare October 3, 2026 00:42
@lusoris
lusoris merged commit 94ef3fd into master Oct 3, 2026
3 of 79 checks passed
@lusoris
lusoris deleted the port/motion-five-frame-window-gpu-twins branch October 3, 2026 00:43
lusoris added a commit that referenced this pull request Oct 3, 2026
…g-tidy baseline (#1915)

* refactor(motion): keep motion_window.h's typedef inside the SYCL clang-tidy baseline

#1893 made integer_motion_sycl.cpp and integer_motion_v2_sycl.cpp include
motion_window.h. clang-tidy then reports modernize-use-using on its
`typedef struct VmafMotionWindow` when it reads the header from those C++
units, which puts the sycl lane at 155 warnings against a baseline of 154.
The header is also included by C translation units, where `using` does not
exist. It therefore takes the same cited NOLINT block that feature_collector.h
uses (ADR-1138), rather than a rewrite.

No behaviour change. `scripts/dev/tidy-lane.sh sycl` in the dev container
reports 154 warnings, matching the baseline (exit 0); on master it exits 2
with motion_window.h at 0 -> 1.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant