Skip to content

cambi: CUDA feature extractor for CAMBI (fixes #1567) - #1571

Open
samfrazerdutton wants to merge 8 commits into
Netflix:masterfrom
samfrazerdutton:cambi-cuda
Open

cambi: CUDA feature extractor for CAMBI (fixes #1567)#1571
samfrazerdutton wants to merge 8 commits into
Netflix:masterfrom
samfrazerdutton:cambi-cuda

Conversation

@samfrazerdutton

Copy link
Copy Markdown

Fixes #1567. libvmaf_cuda had no CAMBI feature extractor, so loading a VMAF
v1 model failed at init with could not initialize feature extractor "Cambi_feature_cambi_score".

Per-frame cambi scores from the CUDA path are bit-identical to the CPU
extractor across a 75-frame 1080p CRF-40 clip, with cambi_high_res_speedup
both 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.h by the preceding commit, per the direction in #1567 — no duplicated
setup. CambiState is the first member of CambiStateCuda, so
cambi_options[] is shared unchanged.

Pooling stays on the host deliberately. cambi_spatial_pooling runs
quick_select and then sums the top-k in whatever order the partition left
them
, accumulating into a double. Float addition isn't associative, so
matching the CPU bit-for-bit would mean reproducing quick_select's partition
ordering.

c_values is one kernel, not a ported histogram. Tracing the CPU's four
phases, the invariant at output row i is a clipped 2-D box count over value
bins; the ring buffer and column scatter are an incremental encoding of it.
And c_value_pixel reads only the bins at compact_v + all_diffs[...]
typically 9 out of a v_band_size in the hundreds. So nothing is
materialised: 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_async discards the caller's stream:

cuMemcpyHtoDAsync(buf->data, src, buf->size,
                  c_stream == 0 ? c_stream : cu_state->str);

Any extractor running kernels on its own stream therefore has an
unsynchronised upload. vmaf_cuda_buffer_download_async has the same shape.

Here it produced non-deterministic output — the derivative kernel reading
d_img[0] mid-upload. It survived full kernel serialisation and reported
clean under compute-sanitizer --tool initcheck (the memory is initialised,
with the previous frame's data). This PR uses cuMemcpyHtoDAsync /
cuMemcpyDtoHAsync directly. Happy to file separately — it may affect the
existing CUDA extractors.

Testing

test/test_cambi_cuda.c gates each kernel bit-exact against the CPU
implementation, 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_values again on a generated frame in
the 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 wrong
ones — which is exactly what one bug here was (TVI thresholds derived at
8-bit when cambi_preprocessing emits 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 the
driver 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 separate
extractor — happy to open an issue for it.

samfrazerdutton 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.
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.

Feature request: CUDA feature extractor for CAMBI

1 participant