speed: CUDA chroma SpEED-QA feature extractor - #1574
Open
samfrazerdutton wants to merge 5 commits into
Open
samfrazerdutton wants to merge 5 commits into
samfrazerdutton wants to merge 5 commits into
Conversation
added 5 commits
August 10, 2026 01:25
filter_and_downscale() in speed.c is built from the shared float vif_* primitives; these are their device counterparts, each gated bit-exact against the CPU implementation in test/test_speed_cuda.c. speed_filter1d_v / _h vif_filter1d_s, separable, mirrored edges speed_dec16 vif_dec16_s (decimation by sixteen) speed_subtract subtract_image speed_picture_copy_u8 picture_copy, 8-bit speed_picture_copy_u16 picture_copy, 10/12/16-bit The CPU accumulates the filter taps with `accum += fcoeff * imgcoeff`, which nvcc would contract into an FMA and round once rather than twice. Rather than depend on -fmad=false reaching this file the arithmetic is spelled out with __fmul_rn/__fadd_rn, and one thread per output walks the taps in the same order, so the summation matches by construction. These are the float vif_* primitives; the existing CUDA VIF is integer_vif, so they are new and reusable beyond SpEED.
Moves SpeedDimensions, SpeedResultBuffers, SpeedBuffers, SpeedOptions, SpeedState, SpeedChromaState and the compute_cov_kernel_fn typedef into a new speed.h, and gives est_params and get_speed_score external linkage so a CUDA extractor can run filter_and_downscale on the device and score on the host. speed_init, speed_extract_score and speed_close were already external and only needed declaring; speed_get_antialias_filter is already public in vif_tools.h. options_chroma becomes speed_chroma_options so both extractors share one table. No functional change. meson test: 21/21. speed_chroma_uv scores 22.705135 / 21.056826 / 25.779747 before and after.
Moves the Speed* structs and the compute_cov_kernel_fn typedef into a new speed.h, gives est_params and get_speed_score external linkage, and renames options_chroma to speed_chroma_options so both extractors share one table. NUM_SCALES is exposed as NUM_SPEED_SCALES. No functional change. meson test: 21/21.
Registers vmaf_fex_speed_chroma_cuda. speed_chroma_u/_v/_uv are identical to the CPU extractor across a 75-frame 1080p clip. Device: picture_copy, the two separable filters, decimation and subtraction. Host: est_params and get_speed_score. Profiling put est_params at 28% of the extractor and the filtering at ~72%, and est_params estimates one covariance matrix and runs one eigen-decomposition per scale rather than per block, so the sequential numerics are O(1) and stay where they already work. Preserves speed_extract_score's filter(ref)/est(ref)/filter(dis)/est(dis) ordering, since est_params writes into SpeedState::buffers. speed_prescale != 1.0 is rejected at init; that path has no device kernel.
vmaf_cuda_picture_upload_async was called with a 0x1 channel mask, so only the luma plane ever reached the device. Every CUDA extractor to date is luma-only, so this went unnoticed; a chroma feature reads zeroed planes and est_params returns -EINVAL on the degenerate covariance. Adds VMAF_FEATURE_EXTRACTOR_CHROMA so extractors declare what they read, and builds the upload mask in translate_picture_host from the registered extractors' flags. Luma-only pipelines are unaffected: vmaf_v0.6.1 over 75 frames runs 0.68 s median with this change against a 0.71 s baseline, versus 0.75 s if chroma were uploaded unconditionally.
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.
Adds a CUDA
speed_chromaextractor. Together with #1571 (CUDA CAMBI) this iswhat a VMAF v1 model needs in order to load on
libvmaf_cudaat all.speed_chroma_u,_vand_uvare identical to the CPU extractor acrossa 75-frame 1080p clip.
Chroma on the CUDA path
libvmaf.ccalledvmaf_cuda_picture_upload_async(pic_device, pic, 0x1), soonly the luma plane has ever reached the device. Every CUDA extractor to
date — VIF, ADM, Motion — is luma-only, so this was never visible. A chroma
feature reads zeroed planes;
est_paramsthen returns-EINVALon thedegenerate covariance and every score comes out 0.
Rather than upload all three planes unconditionally, extractors now declare
what they read:
VMAF_FEATURE_EXTRACTOR_CHROMA, withtranslate_picture_hostbuilding the upload mask from the registered extractors' flags. Luma-only
pipelines are unaffected —
vmaf_v0.6.1over 75 frames runs 0.68 s medianwith this change against a 0.71 s baseline, versus 0.75 s if chroma were
uploaded unconditionally. (Wall clock including I/O, three runs — indicative
rather than tight.)
Where the work runs
Profiling put
est_paramsat 28% ofspeed_chromaand the filtering aroundit at ~72%. Within
est_paramsthe covariance is estimated once per scaleacross all blocks and there is one eigen-decomposition — O(1) per scale,
not per block.
So the device runs the per-pixel front end (
picture_copy, two separablefilters, decimation, subtraction) and
est_params/get_speed_scorestay onthe host, where the awkward numerics already work: a float reduction in
compute_meanwhose summation order a parallel reduction wouldn't preserve,and an iterative QR eigensolver. Cost is one download of the filtered plane
per picture per channel — 1/16 the source in each axis after
dec16.The interleaving of
speed_extract_scoreis preserved — filter(ref),est(ref), filter(dis), est(dis) — since
est_paramswrites intoSpeedState::buffers.speed_prescale != 1.0is rejected at init rather than silently running adifferent pipeline; that path uses
vif_scale_frame_*, which has no kernelhere.
Testing
test/test_speed_cuda.cgates each kernel bit-exact against the CPU:filter1d(both passes, six filter widths),dec16including non-multiplesof 16,
subtract, andpicture_copyfor 8/10/12/16-bit. Strides are paddedand differ between source and destination so a stride bug can't hide.
The CPU accumulates filter taps with
accum += fcoeff * imgcoeff, which nvccwould contract into an FMA and round once instead of twice. The kernels spell
the arithmetic out with
__fmul_rn/__fadd_rnrather than depending on-fmad=false, and one thread per output walks the taps in the same order.Verified that the AVX2 and scalar CPU paths agree bit-for-bit
(
--cpumask 4294967295), so "identical to the CPU" is unambiguous here.These are the float
vif_*primitives; the existing CUDA VIF isinteger_vif, so they're new and reusable beyond SpEED.