Research digest: ADR-0552 — HIP VIF deterministic wavefront reduction¶
Problem characterisation¶
The integer_vif_hip horizontal kernel issued one atomicAdd per thread (128 per row per field) into seven shared int64_t accumulator fields in global device memory. AMD hardware serialises concurrent atomicAdd calls to the same address as compare-and-swap (CAS) loops. While the final integer sum is mathematically associative, the CAS retry ordering within a 64-lane wavefront is non-deterministic in the presence of high contention. This introduced per-frame per-feature jitter of 0.001–0.014, which the VMAF SVM amplified via VIF scale coefficients (1.2–2.1 per scale × 4 scales) to approximately 0.031 VMAF-score divergence from CPU — a 200× ADR-0214 gate violation.
CUDA twin analysis¶
The CUDA twin (filter1d.cu) avoids this with warp_reduce in cuda_helper.cuh:
__forceinline__ __device__ int64_t warp_reduce(int64_t x)
{
for (int i = 16; i > 0; i >>= 1) {
x += int64_t(__shfl_down_sync(0xffffffff, x & 0xffffffff, i)) |
int64_t(__shfl_down_sync(0xffffffff, x >> 32, i) << 32);
}
return x;
}
After reducing, only threadIdx.x % VMAF_CUDA_THREADS_PER_WARP == 0 issues the atomicAdd. This reduces 128 atomic calls to 4 (one per 32-lane warp) and makes the result deterministic.
HIP adaptation¶
AMD GCN/RDNA default wavefront size = 64 (not 32). Differences from CUDA:
| Feature | CUDA | HIP (AMD) |
|---|---|---|
| Warp/wavefront size | 32 | 64 |
| Reduction intrinsic | __shfl_down_sync(mask, val, stride) | __shfl_xor(val, stride) |
| Sync mask | Required (0xffffffff) | Not needed (wavefront is inherently lock-step) |
| Strides for full reduction | 16, 8, 4, 2, 1 | 32, 16, 8, 4, 2, 1 |
The ported helper wavefront_reduce_i64 splits the int64_t into low/high 32-bit halves, reduces each independently with __shfl_xor, then reassembles — identical in structure to the CUDA twin.
Wavefront divergence hazard¶
__shfl_xor requires all 64 lanes in the wavefront to execute it at the same program counter. The original kernels used an early return for out-of-bounds pixels. Early-returning some lanes causes wavefront divergence: the remaining lanes stall waiting for the diverged lanes to reconverge, and the XOR-shuffle reads the diverged lanes' register files in an undefined state.
Fix: remove the early return; wrap the computation in if (x < w && y < h); initialise the accumulator struct to zero. Out-of-bounds threads contribute zero to the reduction — correct under integer addition.
Verification¶
Compilation test: hipcc --genco --offload-arch=gfx1036 succeeds with no errors or warnings against the modified kernel.
Numerical verification (to be run in the vmaf-dev-mcp container per the scope specification):
docker exec vmaf-dev-mcp /workspace/build-hip/core/tools/vmaf \
--backend hip \
--reference /workspace/testdata/ref_576x324_48f.yuv \
--distorted /workspace/testdata/dis_576x324_48f.yuv \
--width 576 --height 324 --pixel_format 420 --bitdepth 8 \
--json /tmp/hip.json
# Compare against CPU baseline (VMAF=94.32301); must be within 1e-4
Decision matrix cross-reference¶
See ADR-0552 ## Alternatives considered for the full option table.