Skip to content

cuda: motion score diverges from CPU — edge mirror off-by-one in calculate_motion_score_kernel_{8,16}bpc #1562

Description

@dsummer

Summary

calculate_motion_score_kernel_8bpc / _16bpc (libvmaf/src/feature/cuda/integer_motion/motion_score.cu) diverge from the CPU integer_motion reference on every resolution and bit depth, because the CUDA kernel was never updated when the CPU motion pipeline changed. GPU vs CPU (--gpumask 0 vs --gpumask 4294967295) on 8-bit crowd_run-derived clips:

resolution │ pooled VMAF (CPU / GPU) │ max per-frame integer_motion2
854×480 │ 48.153502 / 48.153631 │ 2.229e-03
1920×1080 │ 51.548181 / 51.548218 │ 6.620e-04
3840×2160 │ 56.689925 / 56.690007 │ 2.040e-04

The delta shrinks as ~1/width — the signature of a border artifact, not a per-pixel one.

This is distinct from #1552 (16bpc row-stride / OOB). #1552 fixes only the 16bpc addressing; the two issues below remain on both kernels after it lands.

Root cause 1 — edge mirror off-by-one (the dominant term)

The device mirror() uses the pre-fix edge form:

// motion_score.cu
device forceinline int mirror(const int idx, const int sup) {
int out = abs(idx);
return (out < sup) ? out : (sup - (out - sup + 1)); // == 2*sup - out - 1
}

The CPU reference is reflect_101:

// integer_motion.c
static inline int mirror(int idx, int size) {
if (idx < 0) return -idx;
if (idx >= size) return 2 * size - idx - 2; // note: -2, not -1
return idx;
}

Commit a44e5e6 ("libvmaf/feature: port motion updates, bugfix for edge mirroring") changed the CPU edge handling from - 1 to - 2 and never touched the .cu.
This accounts for essentially the entire delta above (a host-side reimplementation isolating the two mirror variants reproduces the full 2.2e-3 at 854×480,scaling as 1/width). It affects the 8bpc kernel too, so it is not covered by #1552.

Root cause 2 — stale blur-first pipeline (~1e-6 residual)

Commit a4a1492 ("replace integer_motion with pipelined v2 variant") changed the CPU to convolve the source difference (prev − cur) with a single rounding
step. The CUDA kernel still blurs each frame separately into a uint16 buffer and subtracts the two blurred results, which rounds differently. After the
mirror is corrected, this leaves a ~1e-6 residual per frame.

Reproduce

BIN=libvmaf/build_cuda/tools/vmaf
$BIN --gpumask 4294967295 -n --feature 'motion=debug=true' -r ref.y4m -d dist.y4m --json -o cpu.json
$BIN --gpumask 0 -n --feature motion -r ref.y4m -d dist.y4m --json -o gpu.json

diff integer_motion / integer_motion2 per frame

Suggested fix

Rewrite both kernels to compute, per pixel, the y-convolution of (prev − cur) then the x-convolution, mirroring with reflect_101 and using an int64
x-accumulator — i.e. the arithmetic of motion_score_pipeline_{8,16} in integer_motion.c. This needs the previous source frame on the device (a packed
2-entry device ring filled with a cuMemcpy2DAsync per frame works; the picture pool is a fixed round-robin so the picture itself can't be retained, and
PREV_REF only supplies a host picture). Doing so makes GPU motion/motion2 bit-exact with the CPU on 8/10/12/16-bit (verified against the AVX-512 reference).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions