Skip to content

fix(gpu): apply motion_fps_weight exactly once on the CUDA/SYCL/HIP motion3 twins - #1375

Merged
lusoris merged 1 commit into
masterfrom
fix/gpu-motion3-fps-weight-double
Sep 7, 2026
Merged

lusoris merged 1 commit into
masterfrom
fix/gpu-motion3-fps-weight-double

Conversation

@lusoris

@lusoris lusoris commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

motion_fps_weight was applied twice on the CUDA, SYCL and HIP motion
twins, so VMAF_integer_feature_motion3_score carried the weight squared.

The CPU reference applies it in exactly one place —
integer_motion.c extract() — scaling the
SAD-derived score and storing the weighted value as motion_sad_score.
flush() reads those weighted values back, takes the neighbour min into
motion2, and blends motion2 into motion3 with motion_blend() and no
second weighting.

All three twins reproduced that blend in a host-side
motion3_postprocess_*() helper that opened with
score2 * s->motion_fps_weight, while every caller in all three already passed
a value that had been fps-weighted and motion_max_val-clipped:

/* caller (CUDA, integer_motion_cuda.c:522) */
double const last_motion2 = MIN(s->score * s->motion_fps_weight, s->motion_max_val);
double const motion3_score = motion3_postprocess_cuda(s, last_motion2);

/* helper — the second application, now removed */
double const weighted = score2 * s->motion_fps_weight;

motion2_score was always correct; only motion3_score drifted.

Why every gate was green

motion_fps_weight defaults to 1.0, and 1.0² = 1.0. Every motion3 parity
test instantiated its extractors with NULL options, so CPU and GPU agreed on a
value neither was computing correctly for any other weight. This is the
option-value analogue of the fixture-shape blind spot in ADR-1204 / ADR-1206 —
written up as research digest 2033.

Reproducer

Restore the second multiplication in any one twin and run its parity test:

meson setup build-cuda core -Denable_cuda=true -Denable_sycl=false -Db_lto=false
ninja -C build-cuda
meson test -C build-cuda test_cuda_motion3_parity --print-errorlogs -v

Measured on the local hardware, 256x144 8-bpc fixture, motion_fps_weight = 0.6
— identical on all three backends:

motion3 mfw=0.6 parity FAIL: cpu=14.48987751 gpu=8.69392654 delta=5.80e+00 tol=1.00e-04

8.69392654 = 14.48987751 × 0.6 — the extra factor exactly.

Verified end-to-end on real silicon: RTX 4090 (CUDA), Arc A380 (SYCL), gfx1030
(HIP, -Denable_hipcc=true). Each fails with the second multiplication restored
and passes with it removed; the pre-existing default-options tests and the SYCL
1080p checkerboard test stay green.

SYCL leg needs the oneAPI env:

source /opt/intel/oneapi/setvars.sh --force
CC=icx CXX=icpx meson setup build-sycl core -Denable_sycl=true -Denable_cuda=false -Db_lto=false
ninja -C build-sycl && meson test -C build-sycl test_sycl_motion3_parity

Scope audit

  • float_motion on every backend already applies the weight once, at the
    emission site — no change needed.
  • The motion_v2 GPU twins have a different, pre-existing seed-frame
    divergence that is already documented in docs/metrics/motion.md; deliberately
    untouched here.
  • Metal has no v1 integer-motion3 post-process to fix.

Deep-dive deliverables (ADR-0108)

Docs (rule 10)

docs/metrics/motion.md gains a normative note in the
v1 motion backend-coverage section stating that the weight is applied exactly
once, what the pre-fix behaviour was, and which test now guards it.

Bug status (rule 13 / ADR-0165)

  • docs/state.md — closes T-GPU-MOTION3-FPS-WEIGHT-SQUARED-2026-09-07.

🤖 Generated with Claude Code

@lusoris
lusoris force-pushed the fix/gpu-motion3-fps-weight-double branch from 4fb5b32 to fe5c5d7 Compare September 7, 2026 05:44
@lusoris
lusoris marked this pull request as ready for review September 7, 2026 05:54
…otion3 twins

The CPU reference scales the SAD-derived motion score by motion_fps_weight in
exactly one place — extract() in integer_motion.c — and stores the weighted
value as motion_sad_score. flush() reads those already-weighted values back,
takes the neighbour min into motion2, and blends motion2 into motion3 without
touching the weight again.

The CUDA, SYCL and HIP twins each reproduced that blend in a host-side
motion3_postprocess_*() helper that opened with `score2 * motion_fps_weight`,
while every caller in all three twins already passed a value that had been
fps-weighted and motion_max_val-clipped. motion3_score therefore carried
motion_fps_weight squared. motion2_score was always correct.

Measured on the local hardware with a 256x144 8-bpc fixture at
motion_fps_weight = 0.6, identical on all three backends:

    cpu = 14.48987751   gpu = 8.69392654   (= 14.48987751 x 0.6)
    delta = 5.80e+00 against the 1e-4 ADR-0214 gate

Every existing gate was green because motion_fps_weight defaults to 1.0 and
1.0 squared is 1.0: all three motion3 parity tests instantiated their
extractors with NULL options, so CPU and GPU agreed on a value neither was
computing correctly for any other weight. Each test now carries a
test_motion3_fps_weight_applied_once variant that pins motion_fps_weight = 0.6
and asserts parity on the ADR-1183-derived integer_motion3_mfw_0.6 key.
Verified on an RTX 4090 (CUDA), an Arc A380 (SYCL) and gfx1030 (HIP): the
variant fails with the 5.8 drift when the second multiplication is restored
and passes with it removed, on all three.

motion_v2 and float_motion were audited and are unaffected — float_motion
already applies the weight once at the emission site, and the motion_v2 GPU
seed-frame divergence is a separate, pre-existing behaviour documented in
docs/metrics/motion.md.

ADR-1216.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lusoris
lusoris force-pushed the fix/gpu-motion3-fps-weight-double branch from fe5c5d7 to ba88239 Compare September 7, 2026 05:56
@lusoris
lusoris merged commit 0eb3855 into master Sep 7, 2026
91 of 92 checks passed
@lusoris
lusoris deleted the fix/gpu-motion3-fps-weight-double branch September 7, 2026 06:39
@lusoris lusoris added the type:bug Something isn't working label Sep 7, 2026
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.

1 participant