cambi: CUDA feature extractor for CAMBI (fixes #1567) - #1571
Open
samfrazerdutton wants to merge 8 commits into
Open
cambi: CUDA feature extractor for CAMBI (fixes #1567)#1571samfrazerdutton wants to merge 8 commits into
samfrazerdutton wants to merge 8 commits into
Conversation
added 8 commits
August 6, 2026 22:05
Moves CambiState, CambiBuffers, the callback typedefs and the NUM_SCALES / PICS_BUFFER_SIZE / MASK_FILTER_SIZE / CAMBI_MIN_WIDTH_HEIGHT macros into cambi.h, and gives eleven helpers external linkage so a CUDA extractor can share one copy rather than duplicating the setup (per discussion in Netflix#1567). Generic names take a cambi_ prefix since they are external symbols now: adjust_window_size, set_contrast_arrays, get_tvi_for_diff, get_vlt_luma, get_mask_index, get_pixels_in_window, spatial_pooling, weight_scores_per_scale, combine_dist_src_scores, dump_c_values. cambi_preprocessing only loses static. options[] becomes cambi_options[] so both extractors share one table; a CUDA state struct with CambiState as its first member keeps every offsetof() valid. test/test_cambi.c #includes cambi.c directly to reach these statics, so its call sites are renamed to match. No test logic changes. No functional change. meson test: 21/21. VMAF v1.0.16_3d0h on a 1080p CRF-40 gradient clip scores 75.281815 before and after.
Adds src/feature/cuda/cambi_cuda.c and registers vmaf_fex_cambi_cuda, so
Cambi_feature_cambi_score resolves on the libvmaf_cuda path. Per-frame scores
are bit-identical to the CPU extractor across a 75-frame 1080p clip, with
cambi_high_res_speedup both off and on.
Device: preprocessing (decimate/convert to 10 bit, anti-dithering), derivative
and spatial mask, then per scale decimate, mode filter and c_values. Host:
setup and spatial pooling, via the shared helpers from cambi.h.
Pooling stays on the host deliberately: cambi_spatial_pooling runs
quick_select then sums the top-k in whatever order the partition left them,
accumulating into a double, so matching it bit-for-bit on GPU would mean
reproducing the partition ordering.
c_values stages its window footprint in shared memory, keyed by a sentinel
that folds masked-out, out-of-band and out-of-image into one compare.
CambiState is the first member of CambiStateCuda so cambi_options[] is shared
unchanged.
Note: transfers use cuMemcpyHtoDAsync/cuMemcpyDtoHAsync directly rather than
vmaf_cuda_buffer_{upload,download}_async, which discard the caller's stream
(c_stream == 0 ? c_stream : cu_state->str) and would leave them
unsynchronised against kernels running on our own stream.
samfrazerdutton
force-pushed
the
cambi-cuda
branch
from
August 9, 2026 03:40
5c0524d to
95f2a52
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1567.
libvmaf_cudahad no CAMBI feature extractor, so loading a VMAFv1 model failed at init with
could not initialize feature extractor "Cambi_feature_cambi_score".Per-frame
cambiscores from the CUDA path are bit-identical to the CPUextractor across a 75-frame 1080p CRF-40 clip, with
cambi_high_res_speedupboth off and on.
Split
Device: preprocessing (decimate + convert to 10-bit, anti-dithering),
derivative and spatial mask, then per scale decimate, mode filter and
c_values.Host: setup and spatial pooling, through the shared helpers exposed in
cambi.hby the preceding commit, per the direction in #1567 — no duplicatedsetup.
CambiStateis the first member ofCambiStateCuda, socambi_options[]is shared unchanged.Pooling stays on the host deliberately.
cambi_spatial_poolingrunsquick_selectand then sums the top-k in whatever order the partition leftthem, accumulating into a
double. Float addition isn't associative, somatching the CPU bit-for-bit would mean reproducing quick_select's partition
ordering.
c_valuesis one kernel, not a ported histogram. Tracing the CPU's fourphases, the invariant at output row
iis a clipped 2-D box count over valuebins; the ring buffer and column scatter are an incremental encoding of it.
And
c_value_pixelreads only the bins atcompact_v + all_diffs[...]—typically 9 out of a
v_band_sizein the hundreds. So nothing ismaterialised: each thread accumulates just those bins as deltas from its own
value, over a window staged in shared memory. No scatter, no atomics, no
row-to-row dependency.
Upstream defect found while debugging
vmaf_cuda_buffer_upload_asyncdiscards the caller's stream:Any extractor running kernels on its own stream therefore has an
unsynchronised upload.
vmaf_cuda_buffer_download_asynchas the same shape.Here it produced non-deterministic output — the derivative kernel reading
d_img[0]mid-upload. It survived full kernel serialisation and reportedclean under
compute-sanitizer --tool initcheck(the memory is initialised,with the previous frame's data). This PR uses
cuMemcpyHtoDAsync/cuMemcpyDtoHAsyncdirectly. Happy to file separately — it may affect theexisting CUDA extractors.
Testing
test/test_cambi_cuda.cgates each kernel bit-exact against the CPUimplementation, keeping the CPU code verbatim — the four-phase incremental
histogram, the cyclic DP matrix — rather than a simplified reimplementation.
Six gates: derivative, decimate, filter_mode, spatial_mask,
c_values(96 synthetic configurations), and
c_valuesagain on a generated frame inthe production parameter regime.
That last one exists because the synthetic sweep supplies its own
tvi_for_diff/band values and so cannot catch a caller passing the wrongones — which is exactly what one bug here was (TVI thresholds derived at
8-bit when
cambi_preprocessingemits 10-bit). All tests are self-contained.Verified on RTX 2060 (sm_75), CUDA 13.2, Ubuntu 24.04/WSL2, from a clean
meson setup.meson test: 21/21, no new warnings. The test reaches thedriver through the ffnvcodec dynlink loader like the rest of the library, so
it needs no CUDA toolkit install.
Out of scope
VMAF v1 models also require a CUDA chroma SpEED-QA extractor (
speed.c)before they can run end-to-end on
libvmaf_cuda. That's a separateextractor — happy to open an issue for it.