Skip to content

cuda: keep kernel parameter structs and accumulators out of local memory - #1595

Open
birkdev wants to merge 1 commit into
Netflix:masterfrom
birkdev:cuda-kernel-params
Open

birkdev wants to merge 1 commit into
Netflix:masterfrom
birkdev:cuda-kernel-params

Conversation

@birkdev

@birkdev birkdev commented Sep 17, 2026 •

Copy link
Copy Markdown

The device helper kernels take AdmBufferCuda, AdmFixedParametersCuda, VifBufferCuda and filter_table_stuct by value. nvcc forwards those copies to param space, but clang's NVPTX backend copies the 584 and 248 byte structs into a local memory depot with a byte loop as soon as they are indexed with a runtime value like blockIdx.z. The filter1d horizontal kernels have the same problem with their seven int64 accumulators: nvcc unrolls that loop on its own, clang doesn't, and the array lands in local memory.

Passing the structs by const reference and adding #pragma unroll on those loops fixes both. No kernel logic changes. With nvcc, resource usage and scores are unchanged (same local memory footprint, integer_adm and integer_vif bit identical before and after). The filter1d SASS is reordered but has the same instruction count. With clang, adm_csf went from 57x slower than nvcc to parity and total kernel time is within 3% of nvcc

This is the first half of making -Denable_nvcc=false usable without the CUDA toolkit headers, see #1596

The __device__ helper kernels took AdmBufferCuda, AdmFixedParametersCuda,
VifBufferCuda and filter_table_stuct by value. nvcc forwards those copies
to the kernel's param space, but LLVM's NVPTX backend materialises the
584 and 248 byte structs in a local memory depot (via a byte-by-byte
ld.param/st.local loop) as soon as they are indexed with a runtime value
such as blockIdx.z. Pass them by const reference instead, so both
compilers read the kernel parameters in place. adm_cm needed a const
pointer for params.i_rfactor as a result.

The filter1d horizontal kernels also reduce their seven 64-bit per-thread
accumulators in a plain loop. nvcc unrolls it on its own; clang keeps it
rolled because the inlined warp_reduce body exceeds its unroll threshold,
which forces the accumulator array into local memory. Mark those loops
with #pragma unroll like the neighbouring ones.

With this, clang-compiled PTX has the same local memory footprint as
nvcc's for every kernel and, measured with Nsight Systems over 600 1080p
frames on an RTX 5090, total kernel time is within 3% of the nvcc build
(178.9 ms vs 183.8 ms); before, adm_csf ran 57x and filter1d 4.5x slower.
nvcc's own output is unchanged, and integer_adm and integer_vif remain
bit-identical between the two compilers.
@birkdev

birkdev commented Sep 17, 2026

Copy link
Copy Markdown
Author

The second part, the clang build path this prepares for, is #1596.

@lusoris

lusoris commented Oct 1, 2026

Copy link
Copy Markdown

Tested on master 6ec23e8f2 (built with -Wno-error=incompatible-pointer-types, which that revision needed with GCC 16; master has since fixed that in 8e7a1ac4e, the only upstream change since) + this PR (merges without conflicts), RTX 4090, CUDA 13.4.92, gcc 16.2.1, clang 22.1.8. Release build with -Denable_cuda=true, build directory libvmaf/build-cuda.

The local-memory claim reproduces. clang++ -x cuda --cuda-gpu-arch=sm_89 --cuda-device-only -O3 -DDEVICE_CODE -Xcuda-ptxas -v, stack frame per kernel, master -> this PR:

file kernels stack frame
adm_csf.cu adm_csf_kernel_1_4, i4_adm_csf_kernel_1_4 832 B -> 0 B (registers 39 -> 21 and 28)
adm_cm.cu adm_cm_line_kernel_8 880 B -> 0 B
filter1d.cu 4x filter1d_16_horizontal, filter1d_8_horizontal 120 B -> 0 B
filter1d.cu 3x filter1d_16_vertical, filter1d_8_vertical 144 B -> 0 B

All other kernels in adm_decouple.cu, motion_score.cu and the rest of filter1d.cu are at 0 B before and after.

nvcc (the default build): cuobjdump -res-usage for sm_80 is identical for all seven fatbins (REG, STACK, LOCAL). The SASS is byte-identical for six of them; filter1d.fatbin differs (same 8410 instructions, different order), so "nvcc output unchanged" holds for resources but not literally for filter1d.

Scores: vmaf_v0.6.1 on the CUDA path, master vs this PR, every metric of every frame compared, 12 metrics per frame: max abs difference 0 on the three Netflix pairs (src01 576x324, 48 frames; checkerboard 1920x1080 1 px and 10 px, 3 frames each) and on the 10-bit src01 pair (3 frames). The CLI prints 6 digits, so this is a comparison at that precision.

meson test: 25 ok, 1 fail (test_cuda_pic_preallocation, SIGSEGV), identical to master (26 tests). That test is the one #1573 fixes.

@birkdev

birkdev commented Oct 1, 2026

Copy link
Copy Markdown
Author

Thanks for reproducing this on your 4090. You're right about filter1d, with nvcc the resource usage and scores are unchanged, but the SASS isn't byte-identical. Most likely that's the added #pragma unroll, since the two ADM files only got the by-reference change and stayed identical. I've reworded the description!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants