Repository navigation
Conversation
7bbfa9f to
8781fe5
Compare
8781fe5 to
6852db9
Compare
…AF alone and with NEG, VMAF v1; RTX 5090, Intel iGPU, CPU), and every claim checked against the code, git and the runs Problem The landing page of 14a1322 had wrong or unsupported statements: - Its speed table came from older runs: libvmaf's CUDA code only from host memory (46 fps at 4K), no route from GPU memory, VMAF v1 on another film (3840x1608), and app-level numbers from VideoMetricsLab. - "Moving a 4K frame pair through system memory costs the CPU more than the GPU half of VMAF v1 saves": not so (the hybrid from system memory is 1.7x the CPU's speed at 4K with fewer cores busy). - "It is also faster than the CUDA code it ports": only for VMAF and NEG together, or from system memory. For VMAF alone from GPU memory, libvmaf's CUDA code is faster (465 against 382 fps at 4K). - "integer arithmetic only": the shaders use float for estimates the result does not depend on (ADM's angle pre-check, VIF's division), and double with the optional NATIVE_F64. - The AMD fault was described as a read "at a negative index"; it is the table's entries for values below -1, read at their usual place. - The self-test was called the engine's; it is the Python bindings' probe(). - VMAF v1's 71-case matrix was said to be identical "on all three GPUs"; the Radeon 780M ran 48 frames of film, not the matrix. - "libvmaf runs the two feature sets separately": it runs motion once and VIF and ADM twice (fex_ctx_vector.c merges extractors with equal options). - libvmaf's CUDA code "within about 0.001" of the CPU: that was before Netflix#1644; now only motion differs, by at most 0.000029 on the benchmark video. - Also: "fully on the GPU" (libvmaf predicts on the CPU); the five-frame window and moving average belong to the HFR models only; Netflix#1612's leak and Netflix#1652's hang were missing; the GPU half of VMAF v1 "alone at 655 fps" and "2.9 cores busy instead of 7.7" could not be reproduced here (dropped); the whole-film check ran before the AMD fix to one shader. - Of the ten shaders, only common.slang carried the Netflix and NVIDIA copyright notice the page says the engine keeps. - Source comments repeated "no float or double on the GPU" and pointed to fast/README.md for the pull requests, which the root README lists. Change - fast/tests/bench_readme.py (new): the page's tables. Frames decoded into memory first, then each route scores them in a loop: libvmaf on the CPU at 4-24 threads; libvmaf's CUDA code from pinned host pictures and from device pictures filled on the GPU; Vulkan from system memory and from GPU memory (its buffers imported into CUDA); VMAF v1 with each GPU. Speed and the process's CPU time per route; scores compared within each kind (GPU routes of VMAF v0.6.1 against libvmaf's CUDA code). --vmaf-only for VMAF without NEG. - README.md rewritten from the measurements and the checks: - Speed: one table for VMAF and VMAF + NEG at 4K and 1080p (CPU, CUDA and Vulkan from system and GPU memory, Intel iGPU), one for VMAF v1 with cores busy, and what they show, including where CUDA is faster. - How it was done: what is reproduced exactly and how (per-pixel float and double, tables, rounding of partial sums), where float is still used, the Intel and AMD driver faults as found, the self-tests as the bindings' probe(), VMAF v1's work split measured on the benchmark video (ADM3 + motion3 68% at 4K, 63% at 1080p), the route from GPU memory. - The pull request table with complete fixes; Netflix#1477 merged upstream on 2026-10-02; which seven changed since; Netflix#1562's second cause. - How it is checked: the two matrices' actual cases, real video, the whole film, and exactly what was run on the Radeon 780M. - fast/vulkan/shaders/*.slang: the libvmaf notice (Netflix 2016-2023, NVIDIA 2021, BSD+Patent) on the nine that lacked it; motion_v1 and vif_filter name the CPU files they come from. common.slang's note on arithmetic says where float and double are used. - fast/vulkan/vmaf_vulkan.cpp, common.slang, build_libvmaf_cuda.ps1: comments point to README.md for the pull requests. - fast/python/vmaf_fast: vulkan.py and v1.py docstrings no longer say "integer arithmetic only", "several times faster" or native/vmaf_vulkan; v1.py drops app numbers from VideoMetricsLab. - fast/README.md: bench_readme.py under Testing. Effect No change to any library: vmaf_vulkan.dll rebuilt from this tree is byte for byte the release's (SHA-256 47b95eeb...), so v3.2.0-fast.1 stays current. The landing page states only what was measured or read here. Verification (RTX 5090, Core Ultra 9 285K and its Intel GPU) - bench_readme.py, HoneyBee 3840x2160 10-bit against its x265 encode, 48 frames in memory: 480 pairs at 4K, 960 at 1920x1080, with and without --vmaf-only. Repeat runs within about 1%. libvmaf's CUDA code from pinned host pictures with 0, 2, 4 and 8 threads: 58-65 fps at 4K. - compare_vmaf_vulkan.py --matrix (45) and compare_vmaf_v1.py --matrix (71), each with --device 0 and 1: ALL IDENTICAL. diagnose_vmaf_vulkan.py: every sum and buffer is the reference's, on both. - Real video, 48 frames of HoneyBee at 4K 10-bit and 1080p 8-bit, both GPUs: VMAF, NEG and VMAF v1 IDENTICAL. - libvmaf CUDA against CPU, feature by feature on the benchmark video: VIF and ADM (VMAF and NEG) bit-identical; motion2 at most 2.87e-5 (4K) and 4.53e-6 (1080p). - VMAF v1 per feature, libvmaf on one thread, 96 frames: ADM3 55.2 ms, motion3 6.0, CAMBI 12.4, SpEED 16.0 at 4K. - The whole Beekeeper film again (VideoMetricsLab's scripts/verify_vmaf_vulkan_movie.py, the release's DLLs, RTX 5090): 151,919 of 151,919 frames, all 11 features bit-identical to CUDA, VMAF and NEG identical (means 94.927743 and 90.299552). The Intel run of 2026-10-04 (earlier build) is cited as such. - The pull requests' states, authors and head commits read with gh on 2026-10-05; upstream's tree has no GPU code but CUDA. Limits - One PC. The Radeon 780M results are the other PC's reports, not re-run. - Cores busy are compared at different speeds; libvmaf's CPU time per frame grows with its thread count, so they are not a per-frame cost. - libvmaf's CUDA code from device pictures crashed in this benchmark with libvmaf's thread pool on (threads > 0); its table row is with threads = 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
libvmaf.h says the context takes ownership of both pictures, but a call that returned an error left them with the caller: a caller that followed the header leaked the pair, and one that took the pair from vmaf_preallocate_pictures() hung in vmaf_close(), which waits for every pool picture. Once the call has a context and two pictures, every return now drops both references through the caller's pointers. vmaf_picture_unref() clears the struct, so a caller that unrefs after the error gets -EINVAL and releases nothing twice. A call without a context, with only one of the two pictures, or the flush call still takes nothing. The new test submits a pair of another shape than the stream's and checks the references, the harmless second unref, the calls that take nothing, and that vmaf_close() returns with a pool pair; its meson entry carries a timeout so a regression fails instead of hanging.
6852db9 to
dd51642
Compare
|
Rebased onto acdd937; the new head is dd51642. The conflict was in libvmaf.c at the top of vmaf_read_pictures(), where convert_pictures() now runs. It is placed after the flushed check, and a failure of it releases both pictures like every other return. Both pictures are released through the caller's pointers, so this holds after a successful conversion too (the converted pictures have replaced the originals in the caller's structs). The new conversion-failure return is not covered by a test of its own; test_read_pictures_release covers the other returns (validation, pool, flushed, one-sided calls) and the pool-pair hang in vmaf_close(). Release build with checkasm: all 28 meson tests pass, and test_read_pictures_release runs 4 tests, all passing. ASan/UBSan build: 24 pass and 4 fail (test_predict, checkasm, test_read_pictures_convert, test_pic_preallocation); the same 4 fail on unpatched master acdd937, and test_read_pictures_release is not among them. |
libvmaf.hsaysVmafContexttakes ownership of both pictures passed tovmaf_read_pictures(). When the call returns an error it does not: it returns with both references still held for the caller. A caller that follows the header leaks the pair, and a caller that took the pair fromvmaf_preallocate_pictures()hangs invmaf_close(). This makes the call release both pictures on every return, so the code does what the header says.What breaks today
Master
8e7a1ac4e, a small C program over the public API (CPU, no feature other thanpsnr): a first 32x32 pair is accepted, then a 64x48 pair is submitted.validate_pic_params()rejects it with-22. The caller does not unref, as the header says.vmaf_picture_alloc(), ASan + UBSan (LeakSanitizer)VmafRefand private data)vmaf_preallocate_pictures()(pool of 2), thenvmaf_close()alarm()ends it (exit 142).vmaf_close()waits invmaf_picture_pool_close()until every pool picture is backThe error returns of
vmaf_read_pictures()that leave the pair with the caller on master:validate_pic_params(),check_picture_pool(),check_ring_buffer()and the referencetranslate_picture()(CUDA builds), an extractor failure in the dispatch loop, and a failed enqueue inthreaded_read_pictures_batch()(it drops the extra references it took itself, not the caller's), and the call after the context was flushed.Fix
Once the call has a context and both pictures, each of those returns drops both references through the caller's pointers (
release_picture_pair()).vmaf_picture_unref()clears the struct it is given (picture.c, thememsetat its end), so a caller that unrefs after the error gets-EINVALand nothing is released twice. A call without a context, with only one of the two pictures, and the flush call (bothNULL) take nothing, as before. Invmaf_read_pictures()theflushedtest moves below the flush branch: a flush after a flush still returns-EINVAL; a pair after a flush is now released as well.On the threaded path a failed
vmaf_thread_pool_enqueue()unrefs the three referencesthreaded_read_pictures_batch()took (pic_a,pic_b, the previous pictures) and returns the error without touching the caller's pair; the caller's references are now released once, invmaf_read_pictures().libvmaf.hgets two sentences: the ownership holds on an error return too, and which calls take nothing.Callers
FFmpeg's
libavfilter/vf_libvmaf.c(master at663a37f7f9d3, the latest commit touching the file; read on 2026-10-01): lines 163-166 and 790-793 log and returnAVERROR(EINVAL)after a failedvmaf_read_pictures()and do not unrefpic_ref/pic_dist(the unref at line 159 is for the earliercopy_picture_data()failure). It follows the header, so it leaked the pair on an error and is correct with this change. In this treetools/vmaf.cdoes not touch the pictures after an error either, andvmaf_close()is not reached on that path (it returns after the flush), so the CLI is unaffected. A caller that unrefs after an error today (the tests added by #1639 and #1621 do) still works: the struct is cleared, the unref returns-EINVALand releases nothing.Test
libvmaf/test/test_read_pictures_release.c(new, registered with a 20 s timeout):-EINVAL;vmaf_close()returns.The pool case hangs on master. The bound is meson's
timeout : 20on thetest()entry, so a regression fails the test instead of hanging the suite; run directly, the binary would block. On master the test fails at the first case ("rejected ref still referenced by the context"); with only the pool case it ends inTIMEOUTafter 20 s; with the change it passes.Validation
x86-64 Linux, GCC 16.2.1, RTX 4090 with CUDA 13.4.
Rebased on master
9e48141b(2026-10-02). The release and sanitizer builds, the new test and the Netflix pair comparison were re-run on it. The new test's failure on unpatched master (withlibvmaf.candlibvmaf.hreverted, LeakSanitizer) was measured oncea2b4d8and not repeated: neither file changed since. The CUDA build line below was measured on8e7a1ac4eand not repeated.meson testwith-Denable_float=true -Denable_checkasm=true: 25/25 on master, 26/26 here (the new test), re-run on9e48141b. Default options (22/22 and 23/23) were measured oncea2b4d8and not repeated.-Db_sanitize=address,undefined -Db_lto=false, same options: master 22 pass and 3 fail of 25; here 23 pass and 3 fail of 26. The failures are the same three on both:test_predictandtest_pic_preallocation(SIGABRT, LeakSanitizer) andcheckasm(heap-buffer-overflow inadm_dwt2_16,integer_adm.c:2603); this change touches none of them..engagement/golden_compare.pymaster binary vs this branch: all identical (default dispatch andcpumask=-1), e.g. src01 vmaf_mean 76.667831 on both.-Denable_cuda=true,libvmaf/build-cuda), under the device lock: it compiles;test_read_pictures_releaseandtest_pic_preallocationpass;test_cuda_pic_preallocationfails with SIGSEGV here and on unpatched master.vmaf --gpumask 0on src01 gives vmaf_mean 76.668905 on both.Not covered
translate_picture_device()leaves the freshly allocated host picture allocated whenvmaf_cuda_picture_download_async()fails; the return value of thedisttranslate_picture()call is never checked; and on an error in the dispatch loop with device input, the host copy made for the CPU extractors is not released. cuda: complete device-picture cleanup after threaded dispatch #1613 does not touch those error paths (it changes the download mask to0x7, adds a stream synchronize that returns-EIOwithout freeing the host picture, and moves the device cleanup after the threaded dispatch). They stay as they are.Relation to other changes
vmaf_read_pictures()(the thread-pool branch); the two conflict textually inlibvmaf.c. The resolution is mechanical: itsif (vmaf->thread_pool)restructure plusrelease_picture_pair()on an error.-EINVALreturn before the pair is touched; with this change that return needs the samerelease_picture_pair()call. libvmaf: reject pictures whose bit depth does not match #1621 (&&to||invalidate_pic_params()) needs nothing: its error comes through the path released here. Both PRs' tests unref after a rejection and ignore the result, so they pass with or without this change.vmaf_cuda_buffer_alloc()fromassert(0)to an error (issue Crash when 2 files are analyzed simultaneously .../src/cuda/common.c:166: vmaf_cuda_buffer_alloc: Assertion `0' failed. #1420); an out-of-memory then reachesvmaf_read_pictures()as an error return, which this change handles. OtherCHECK_CUDAsites, for example invmaf_cuda_picture_alloc()at 400 MiB of free VRAM, still abort.