Repository navigation
Do not wait on the host for every picture the GPU video decoder converts - #6561
Draft
jantonguirao wants to merge 8 commits into
Draft
jantonguirao wants to merge 8 commits into
jantonguirao wants to merge 8 commits into
Conversation
The NVDEC decoder cache looked up decoders by the coded size of the stream (CUVIDEOFORMAT::coded_height/width), but stored the display area size (ulTargetHeight/Width) of the decoder it created. The two differ whenever the stream is cropped, e.g. 1080p H.264 is coded as 1920x1088 and displayed as 1920x1080, so for such videos no cached decoder ever matched. As the cuvidReconfigureDecoder fallback is compiled out (ENABLE_NVDEC_RECONFIGURE is never defined), every video opened by FramesDecoderGpu got a freshly created decoder, and unused ones piled up in the cache. In an nsys profile of experimental.readers.video on 1080p H.264 clips, each file switch stalled the reader for ~70 ms before the first kernel, ~57 ms of which were ioctls inside libnvcuvid, consistent with creating a new decoder. Store both the coded size and the display area in DecInstance and require both to match, so a decoder is reused only for streams it is really suited for. Add NVDECCacheSize() and a test that decodes the same 800x600 (coded 800x608) H.264 file with two consecutive decoders and checks that the second one reuses the cached decoder. Signed-off-by: Joaquin Anton Guirao <janton@nvidia.com>
Comparing the cache size before and after the second decode does not detect a cache miss when the cache is full: the miss inserts a new decoder and evicts an idle one, so the size stays the same. Replace NVDECCacheSize() with NVDECDecodersCreated(), a cumulative count of decoders created by the cache, and check that the second decoder does not increase it. Signed-off-by: Joaquin Anton Guirao <janton@nvidia.com>
Move the check whether a cached decoder can be reused into NVDECCache::IsReusable and document each criterion, in particular why both the coded size and the display area have to match. No functional change. Signed-off-by: Joaquin Anton Guirao <janton@nvidia.com>
FramesDecoderGpu's frame_buffer_ is returned to the memory pool without stream ordering when the decoder is destroyed (the reader replaces its decoder on every file switch), while an asynchronous copy out of it may still be pending on the decoder's stream. Another stream could then be given that memory. The destructor now synchronizes the stream first. Signed-off-by: Joaquin Anton Guirao <janton@nvidia.com>
…_DTOR_CALL As the other destructors that synchronize a stream do: only the error of an unloading CUDA runtime is ignored. Signed-off-by: Joaquin Anton Guirao <janton@nvidia.com>
Planning decode work across lanes needs an estimate of the pictures a decoder decodes for a sample. A frame costs every picture up to its own in decoding order (B-frames), clips with an edit list decode a hidden lead-in before their first frame, a seek can land a GOP before the keyframe, and padding past the last frame reopens the file. The frame index now records, per frame, the position of its packet in decoding order (counting packets hidden by an edit list) and, for the frames SeekFrame seeks to, where a seek really starts decoding (asking the demuxer only for streams with hidden packets). Both decoders count the pictures they decode. FramesDecoderBase::AddDecodeWork mirrors SeekFrame (Reset, kMinimumSeekLeap, same keyframe, backward seek) to estimate the pictures decoded and the reopenings, PositionAt gives the position of a decoder from its next frame, and HandleBoundary is static so that frames can be mapped without a decoder. Tests check the estimate against real decoding on the CPU (exact) and GPU (within one picture). Nothing that affects the output changes. Signed-off-by: Joaquin Anton Guirao <janton@nvidia.com>
…anges below build on Signed-off-by: Joaquin Anton Guirao <janton@nvidia.com>
FramesDecoderGpu synchronized its stream after the color space conversion of every picture before it unmapped the NVDEC output surface, so the host sat idle while each conversion ran. The surface now stays mapped after the conversion is launched, with an event recorded behind it; the wait and the unmap move to ReleaseMappedFrame, which runs right before the next cuvidDecodePicture, in Flush, and in the destructor (on the decoder's device, after its stream sync), before the decoder goes back to the NVDEC cache. NVDEC is still used only after every earlier conversion is done, and at most one surface is mapped at a time. A conversion that fails after the map no longer leaks the mapping. The move constructor is deleted, as the parser calls back into the object. As the decoder no longer waits for its GPU work, the users of the decoder wait for its stream after each sample (decoders.video, readers.video) or at the end of each Run (inputs.video), so that asynchronous errors are reported at the sample that caused them. Tests hold up the decoder's stream so that conversions are pending when the next picture is decoded, when the decoder seeks, flushes and is destroyed (also in another thread, and before NVDEC goes back to the cache), and check that a failed conversion leaves no surface mapped. Signed-off-by: Joaquin Anton Guirao <janton@nvidia.com>
jantonguirao
force-pushed
the
janton/dali-4935-no-picture-wait
branch
from
October 27, 2026 15:57
96274e7 to
9cb6723
Compare
5 of 17 tasks
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.
Category:
Refactoring (Redesign of existing code that doesn't affect functionality)
Description:
Split out of #6545 (decode lanes for
fn.experimental.readers.video), which was too big to review in one piece.Problem.
FramesDecoderGpu::HandlePictureDisplaymaps the NVDEC output surface of each displayed picture, launches the color space conversion on the decoder's stream, and then callscudaStreamSynchronizebefore it unmaps the surface. So for every picture the GPU decoder converts, the host thread stops and waits for the conversion kernel to finish before it can hand the next packet to the parser and NVDEC. Decoding and conversion are serialized with host round trips in between, and the decoding thread is idle for the duration of every conversion.This affects every user of the GPU video decoder (
FramesDecoderGpu):fn.experimental.readers.videowithdevice="gpu", andfn.experimental.decoders.videoandfn.experimental.inputs.videowithdevice="mixed". The cost grows with the number of pictures converted (long samples, high resolution) and it gets in the way of overlapping the work of several decoders, which the decode lanes of #6563 and #6564 rely on.Solution. The wait is moved to the point where it is actually needed: right before NVDEC may overwrite the surface.
CUDAEvent(conversion_done_) is recorded on the stream behind it.FramesDecoderGpu::ReleaseMappedFramewaits for that event and unmaps the surface. It is called right before the nextcuvidDecodePictureand inFlush.DeviceGuard, before the decoder goes back to the NVDEC cache. The destructor may run on a thread where another device is current, hence the device guard (NVDECLeasegets adevice_id()accessor for that).cuvidMapVideoFramereturns, so a conversion that throws after the map (e.g. an unsupported output type) no longer leaks the mapping: before,cuvidUnmapVideoFramewas simply skipped in that case. If the conversion failed and the event was not recorded, the release waits for the whole stream instead.FramesDecoderGpuis deleted (it was defaulted): the parser calls back into the object, and it may now hold a mapped surface.Because the decoder no longer waits for its GPU work, its users now wait for its stream themselves, so that asynchronous errors (conversions, copies) are still reported at the sample that caused them, rather than in the decoder's destructor:
fn.experimental.decoders.video(VideoDecoderBase): after each sample, before its decoder is reset;fn.experimental.readers.video(VideoReaderDecoder): after each sample is decoded;fn.experimental.inputs.video(VideoInput): at the end of eachRun, on the decoder's own stream (exposed by the newFramesDecoderGpu::stream()), so the frames are complete even ifws.stream()is a different stream.This keeps one wait per sample (or per
Run) instead of one per picture. The decoded frames do not change.For tests, a process-wide counter of mapped surfaces is added:
frame_dec_gpu_impl::NVDECFramesMapped().Depends on #6556, #6559 and #6540. Their commits are on this branch until they are merged; please review only this PR's own commit:
The other commits belong to those PRs: Sync the stream before a GPU video decoder frees its frame buffers (#6556), Estimate a video decoder's work from the decoding order in its index (#6559), and the #6540 commits (Fix NVDEC decoder cache never matching cropped videos, Count created NVDEC decoders in the cache reuse test, Explain the NVDEC decoder cache reuse condition, and the merge commit that brings them in).
Additional information:
Affected modules and functionalities:
dali/operators/video/frames_decoder_gpu.h/.cc: deferred unmap (ReleaseMappedFrame,mapped_frame_,conversion_done_,conversion_recorded_), unmap inFlushand the destructor,stream()accessor,NVDECLease::device_id(),frame_dec_gpu_impl::NVDECFramesMapped(), deleted move constructor.dali/operators/video/decoder/video_decoder_base.h: stream sync after each GPU sample (fn.experimental.decoders.video).dali/operators/video/reader/video_reader_decoder_op.cc: stream sync after each GPU sample (fn.experimental.readers.video).dali/operators/video/input/video_input.h: sync of the decoder's stream at the end of each GPURun(fn.experimental.inputs.video).dali/operators/video/frames_decoder_test.cc: new tests.Key points relevant for the review:
ProcessPictureDecode,Flush) and the destructor (before the decoder returns toNVDECCache) must release the mapped surface first, and the release must wait for the conversion that reads it.DeviceGuardfor the decoder's device; the unmap does not throw. A failed unmap is left counted inNVDECFramesMapped, so that tests can see it.cudaStreamSynchronizewhen the conversion failed before the event was recorded (conversion_recorded_ == false).Runstream syncs in the three operators: they are what keeps asynchronous errors attributed to the right sample now that the decoder does not sync per picture.Tests:
New GTests in
dali/operators/video/frames_decoder_test.cc. Except for the last one, they hold up the decoder's stream with a host function (DelayStream) so that conversions are still pending when the decoder decodes the next picture, seeks, flushes or is destroyed, and compare the frames with those decoded without the delay on the default stream (the first two also with the ground truth):FramesDecoderGpuTest.ConversionPendingAtNextPicture: reading in a row, seeking forward and back, reading the last frame, restarting from the beginning and leaping across a short gap;FramesDecoderGpuTest.ConversionPendingAtNextPictureNoIndex: the same from memory without an index (frames go through the decoder's frame buffer);FramesDecoderGpuTest.DecoderReturnedToCacheIsClean: decoders destroyed with a pending conversion return a clean decoder to the NVDEC cache, which the next decoder on another stream reuses (checked withNVDECDecodersCreated);FramesDecoderGpuTest.DecodersDestroyedInAnotherThread: decoders running in their own threads and streams are destroyed from another thread;FramesDecoderGpuTest.SurfaceUnmappedAfterFailedConversion: a conversion that throws after the map does not leave the surface mapped once the decoder is destroyed.All of them also check, through
NVDECFramesMapped, that at most one surface per decoder is mapped and none is left mapped afterwards.FramesDecoderGpuTest.DecoderCacheReusedForCroppedVideo(from #6540) only builds its file path withmake_stringnow; its checks are unchanged.Existing tests that cover the change:
frames_decoder_test.cc:FramesDecoderGpuTest.*dali/test/python/test_video_reader.pyanddali/test/python/test_video_pipeline.py: GPUfn.experimental.readers.videodali/test/python/decoder/test_video.py: GPUfn.experimental.decoders.videodali/test/python/input/test_video.py: GPUfn.experimental.inputs.videoChecklist
Documentation
DALI team only
Requirements
REQ IDs: N/A
JIRA TASK: DALI-4935