Skip to content

fix(cuda): add the float_motion SAD in the CPU's order so the twin is bit-identical - #1699

Merged
lusoris merged 1 commit into
masterfrom
fix/cuda-float-motion-cpu-float-sum
Oct 1, 2026
Merged

lusoris merged 1 commit into
masterfrom
fix/cuda-float-motion-cpu-float-sum

Conversation

@lusoris

@lusoris lusoris commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

float_motion_cuda now returns the CPU float_motion scores bit for bit: motion, motion2 and motion3 are identical on every frame measured, with no measurable change in time.

Builds on #1695 (every CUDA fatbin without FMA contraction, landed as 691ca5677): the blur is the CPU's only with that flag.

Closes T-CUDA-FLOAT-MOTION-CPU-FLOAT-SUM-2026-10-01, which #1695 found. The CPU extractor adds the absolute differences of a row into one float, the row sums into a second float, and divides in float. Those running sums round at every step, so the score depends on the order of the additions. The twin summed each 16x16 block on the device and the blocks in double on the host, which is closer to the exact mean and up to 1.36e-4 from the CPU on 1920x1080 checkerboards, above the 5e-5 tolerance of ADR-0214. The gate runs the Netflix pair only (3.1e-6), so it passed.

What changed

  • core/src/feature/cuda/float_motion/float_motion_score.cu: the two blur kernels write the blurred plane only. A new float_motion_row_sad kernel runs one thread per row and adds |cur[j] - prev[j]| left to right into one fp32 accumulator; the readback is one float per row.
  • core/src/feature/float_motion_sad.h (new, backend neutral): the tail of compute_motion_simd(), one float over the rows and a float division. float_motion_cuda.c keeps no sum of its own.
  • scripts/ci/cross_backend_calibration.py: EXACT_TWINS lists float_motion: cuda, so the parity gate compares that cell with tolerance 0 at --precision max.
  • ADR-1409 records the contract. The SYCL, HIP and Metal twins still sum per block (T-GPU-FLOAT-MOTION-CPU-FLOAT-SUM-2026-10-01, opened here).

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 and clang-tidy on the touched files: 0 warnings in them; the commit hooks.)
  • Unit tests pass: python3 scripts/ci/run_meson_test.py -- -C build. (--suite=fast on a CUDA build, RTX 4090: 264 of 264 on the current tip.)
  • If I touched any SIMD/GPU code path, I ran /cross-backend-diff and the worst ULP is ≤ 2. (0; table below.)
  • If I touched a feature extractor with SIMD/GPU twins, I either updated every twin or listed the gap under "Known follow-ups" below. (SYCL, HIP and Metal are listed there.)
  • If I added a new .c / .cpp / .cu / .h / .hpp, it has the appropriate license header (see CONTRIBUTING.md).
  • If this is a breaking change, the commit message uses ! or BREAKING CHANGE: and the migration path is documented below. (Not a breaking change.)
  • If this PR adds an ADR, the ADR row lives in docs/adr/_index_fragments/<NNNN-slug>.md.

Bug-status hygiene (ADR-0165)

  • docs/state.md updated in this PR: T-CUDA-FLOAT-MOTION-CPU-FLOAT-SUM-2026-10-01 moved to Recently closed with the evidence; T-GPU-FLOAT-MOTION-CPU-FLOAT-SUM-2026-10-01 opened for the other three twins.

Netflix golden-data gate (ADR-0024)

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

Cross-backend numerical results

RTX 4090, nvcc 13.4, --precision max, float_motion on --backend cpu against float_motion_cuda. Frames identical and largest absolute difference over motion, motion2 and motion3. "Before" is master 691ca5677 (#1695).

fixture                                   frames  before                    after
Netflix 576x324                           48      at most 1/48, 3.1e-6      48/48, 0
checkerboard 1 px, 1920x1080              3       at most 1/3,  1.36e-4     3/3,   0
checkerboard 10 px, 1920x1080             3       at most 1/3,  1.36e-4     3/3,   0
BBB 3840x2160                             200     at most 1/200, 2.4e-5     200/200, 0
gradient and noise 1920x1080              6       -                         6/6,   0
gradient and noise 1280x720, 10-bit       4       -                         4/4,   0
Netflix crop 573x163, 4:4:4               48      -                         48/48, 0
Netflix, motion_fps_weight=0.5 mmxv=3     48      -                         48/48, 0
Netflix, blend factor 0.5, offset 2       48      -                         48/48, 0

The one frame that matched before is frame 0, whose motion and motion2 are 0 by definition.

scripts/ci/cross_backend_parity_gate.py --features float_motion --backends cpu cuda reports tol=0.0e+00 (exact:ADR-1397) max_abs_diff=0.000e+00 OK on all four fixtures (BBB: all 200 frames).

Performance (if perf or feat)

Not a performance change; measured because the row kernel uses one thread per row. vmaf tool, BBB 3840x2160, 200 frames, 25 alternating before/after pairs, host load 12 to 14:

twin                 before   after   paired difference (median, quartiles)
float_motion_cuda    3.00     2.98    -0.12 ms  (-0.57 .. +0.25)
float_psnr_cuda      2.84     2.80    -0.03 ms  (-0.23 .. +0.49)   untouched control

Deep-dive deliverables (ADR-0108)

  • Research digest — no digest needed: the cause was analysed in Research-1403 (the host replay of the checkerboard), and the measurements of the fix are in ADR-1409 and the docs/state.md row.
  • Decision matrix — ADR-1409 ## Alternatives considered.
  • AGENTS.md invariant note — core/src/feature/cuda/AGENTS.md (one thread per row, plain loop, host helper, no other reduction), scripts/ci/AGENTS.md (EXACT_TWINS), and docs/development/rebase-sensitive-invariants.md.
  • Reproducer / smoke-test command — pasted below under "Reproducer".
  • CHANGELOG fragment — changelog.d/fixed/cuda-float-motion-cpu-float-sum.md.
  • Rebase note — docs/rebase-notes.md, ADR-1409.

Reproducer

meson setup build-cuda core -Denable_cuda=true -Denable_sycl=false --buildtype=release
ninja -C build-cuda
# Fail on master by 2.0e-5 and 6.2e-5; need a CUDA device.
build-cuda/test/test_cuda_float_motion_parity
build-cuda/test/test_cuda_float_motion_parity_large
# No device needed.
build-cuda/test/test_float_motion_sad
python3 -m pytest core/test/test_cuda_kernel_source_contract.py scripts/ci/test_cross_backend_parity_gate.py
# The gate cell, tolerance 0.
python3 scripts/ci/cross_backend_parity_gate.py --vmaf-binary build-cuda/tools/vmaf \
  --reference python/test/resource/yuv/checkerboard_1920_1080_10_3_0_0.yuv \
  --distorted python/test/resource/yuv/checkerboard_1920_1080_10_3_1_0.yuv \
  --width 1920 --height 1080 --features float_motion --backends cpu cuda

Known follow-ups

  • float_motion_sycl, float_motion_hip, float_motion_metal still sum per block: T-GPU-FLOAT-MOTION-CPU-FLOAT-SUM-2026-10-01. The host helper is backend neutral; each twin needs the row kernel; the blur is already built without contraction on SYCL (ADR-1367) and HIP (fix(hip): compile every HIP kernel with contraction off through one shared flag list #1694).
  • The gate's float_motion cell compares motion and motion2; motion3 is covered by test_cuda_float_motion_parity, because the SYCL twin does not emit it yet.

@lusoris
lusoris force-pushed the fix/cuda-float-motion-cpu-float-sum branch 3 times, most recently from 8cf21b7 to 1e52ad6 Compare October 1, 2026 13:08
@lusoris
lusoris marked this pull request as ready for review October 1, 2026 13:08
… bit-identical

float_motion.c adds the absolute differences of a row into one float,
the row sums into a second float, and divides by the pixel count in
float. Those running sums round at every step, so the score depends on
the order of the additions. float_motion_cuda summed each 16x16 block
on the device and the blocks in double on the host: closer to the exact
sum, and up to 1.36e-4 from the CPU on 1920x1080 checkerboards, above
the 5e-5 cross-backend tolerance (ADR-0214).

ADR-1409:

- float_motion_score.cu: the blur kernels write the blurred plane only.
  A new float_motion_row_sad kernel runs one thread per row and adds
  |cur[j] - prev[j]| left to right into one fp32 accumulator; the
  readback is one float per row.
- float_motion_sad.h (new, backend neutral): the tail of
  compute_motion_simd(), one float over the rows and a float division.
  float_motion_cuda.c keeps no sum of its own.
- EXACT_TWINS lists float_motion: cuda, so the parity gate compares the
  cell with tolerance 0 at --precision max.

The blur was already the CPU's once every fatbin was built without FMA
contraction (ADR-1403); this change depends on that flag.

Measured on an RTX 4090 against --backend cpu at --precision max,
motion / motion2 / motion3, before and after:

- Netflix 576x324, 48 frames: 3.1e-6 -> identical on every frame
- 1920x1080 checkerboards, 3 frames each: 1.36e-4 -> identical
- BBB 3840x2160, 200 frames: 2.4e-5 -> identical
- also identical: 1280x720 10-bit, a 573x163 4:4:4 crop, and the
  Netflix pair with the fps-weight, cap and blend options set

Time per BBB 3840x2160 frame, 25 paired 200-frame runs: 3.00 ms before,
2.98 after (paired difference median -0.12 ms, quartiles -0.57 to
+0.25; the untouched float_psnr_cuda read 2.84 and 2.80).

Tests: test_cuda_float_motion_parity and its 960x540 variant compare
with == at 8, 10 and 12 bits and fail on the old twin by 2.0e-5 and
6.2e-5; test_float_motion_sad pins the host helper without a device;
test_cuda_kernel_source_contract.py has five planted regressions.

docs/state.md: T-CUDA-FLOAT-MOTION-CPU-FLOAT-SUM-2026-10-01 closed;
T-GPU-FLOAT-MOTION-CPU-FLOAT-SUM-2026-10-01 opened for the SYCL, HIP
and Metal twins, which still sum per block.
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