Repository navigation
Decode the video reader's batches with up to num_threads decode lanes - #6563
Draft
jantonguirao wants to merge 12 commits into
Draft
jantonguirao wants to merge 12 commits into
jantonguirao wants to merge 12 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>
This was referenced Oct 27, 2026
…_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>
The GPU video decoder mapped and converted every picture it buffered, and gave each one a frame buffer, although many are never read with their data: pictures hidden by an edit list, frames only skipped on the way to the frames to read, or frames shown before the next one to read. Decoding them is still needed for the pictures that refer to them, but mapping and converting them is not. The decoder now knows the frames it reads next (frames_to_read_, set by DecodeFrames and SeekFrame) and keeps only the timestamp of a buffered frame that cannot be read with its data (CanBeReadWithData, BufferedFrame::has_data_); frame buffers are attached lazily, reusing spare ones. H.264 non-reference pictures that are not read with their data are not decoded at all (IsPictureReadWithData); they are counted by SkippedPictures, and ParsedPictures (decoded plus skipped) is what the decode-work estimate is checked against. Reading a frame without its data is an internal error. Tests compare the frames read with those of a decoder mapping every picture, and check that pictures not read are neither mapped nor given a frame buffer. Signed-off-by: Joaquin Anton Guirao <janton@nvidia.com>
FrameIndexCache::find returned an iterator after releasing its lock and callers copied the index through it without the lock, while insert() could overwrite the same entry (two readers indexing the same file) or rehash the map. The cache moves to its own files. GetOrBuild builds every index once per file, outside the lock (a shared_mutex, so lookups run in parallel), and returns an immutable shared_ptr; Find never builds nor waits. Indices are kept for the lifetime of the process, as before, and an index is built again when the file's size or modification time changed, taken before the file is opened. instance() is defined in a .cc file, so that there is one cache per process. Each reader holds the indices of its files and decodes with those, without cache lookups after setup, so an index cannot change under a reader. Signed-off-by: Joaquin Anton Guirao <janton@nvidia.com>
The decoder capabilities do not change, but cuvidGetDecoderCaps is serialized in the driver and was queried for every file, which gets slow when many decoders open files at once. Cache the result per device, codec, chroma format and bit depth, and hold the lock during the query so that decoders that start at once query each configuration only once. Used by NVDECCache::GetDecoder and SelectVideoStream. Signed-off-by: Joaquin Anton Guirao <janton@nvidia.com>
The video reader decoded every sample of a batch, one after another, with a single decoder. Now it groups the samples of a batch into runs (samples of one file that one decoder decodes in a row without seeking) and has up to num_threads decode lanes decode them in parallel, each with its own decoder, stream and scratch, waiting for all of them at the end of the batch. The runs are assigned to the lanes by PlanLanes (decode_lane_planner.h): longest first, each to the lane that would finish it first, costed in the pictures its decoder decodes (simulated with AddDecodeWork in the order the lane decodes them), plus a cost per opening of a decoder. Which samples a lane decodes depends only on the samples and on where the lanes' decoders are, so the output never depends on thread timing. A lane without runs releases its decoder. There are at most max_batch_size lanes and, on the GPU, two per NVDEC engine (NumEngines). A sample repeated by pad_last_batch is decoded once, also when it is repeated in the next batch. An error is reported for the first failing sample of the batch, whatever the lanes' timing. Signed-off-by: Joaquin Anton Guirao <janton@nvidia.com>
jantonguirao
force-pushed
the
janton/dali-4935-lanes
branch
from
October 27, 2026 15:57
400e7a0 to
377d0c7
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.
Category:
New feature (non-breaking change which adds functionality)
Description:
fn.experimental.readers.video(CPU and GPU) decodes the samples of a batch in parallel, with up tonum_threadsdecoders ("decode lanes"), and waits for all of them at the end of the batch.Problem. Before this PR, the reader decoded every sample of a batch one after another, with one decoder, one CUDA stream and one thread.
num_threadshad no effect on decoding, and only one NVDEC session per reader was ever active. Video decoding is the bulk of the reader's work, so with large batches, or with samples spread over many files, the reader is limited by one decoder's throughput, while the CPU cores (CPU backend) or the NVDEC engines (GPU backend) that are available stay idle.Solution. The loader still only describes the samples. The reader now plans each batch and decodes it on its lanes. Opening a video decoder is costly and a decoder decodes consecutive samples of a file without seeking, so the unit of work is a run, not a sample:
PlanBatch). Samples of one file that a decoder reaches without seeking form a run: asSeekFramedecides, at mostkMinimumSeekLeapframes ahead or in the same keyframe segment (CanReachWithoutSeek). A sample thatpad_last_batchrepeats in the batch is decoded once, and a sample that it repeats into the next batch is not decoded again (aplannedflag on the sample). Otherwise two lanes could decode the same sample at once, or a repeat could overwrite the data of a batch that may still be in use.PlanLanes, newdecode_lane_planner.{h,cc}).FramesDecoderBase::AddDecodeWorkin the order the lane decodes them. That counts B-frames, pictures hidden by an edit list, where a seek really lands and decoder resets, plus a fixed cost (kOpenCost) per opening or reset of a decoder. Without an index, a run costs its frame range.DecodeBatch,DecodeLanePart). Every lane has its own decoder, CUDA stream (GPU), constant frame and scratch, and decodes its runs on a thread pool with one thread per lane. When only one lane has work in a batch (always withnum_threads=1), it decodes on the calling thread, as before. The batch is done when every lane is. A batch's error is the one of its first failing sample in batch order, whatever the lanes' timing.The assignment depends only on the samples and on where the lanes' decoders are, never on thread timing, so neither does the output. The new tests check that the output is the same with one and with several threads.
Number of lanes:
min(num_threads, max_batch_size)(at least one) and, on the GPU, at most two per NVDEC engine of the device (newFramesDecoderGpu::NumEngines, fromCUVIDDECODECAPS::nNumNVDECs). Withnum_threads=1there is one lane: one decoder, decoding on the calling thread, as before. The one difference is the order within a batch: the samples are decoded grouped by file and by first frame, not in batch order, so the samples of a file share one decoder even when they are not consecutive in the batch.Every batch waits for its slowest lane. Running the lanes on their own threads and decoding a batch ahead is done in #6564.
This PR is split out of #6545.
Depends on #6558, #6560 and #6562, and on what they build on. Their commits are also on this branch until they merge:
AddDecodeWork,PositionAt, used by the planner)ParsedPictures, used for the lanes' diagnostics)This PR's own change is the last commit only: "Decode the video reader's batches with up to num_threads decode lanes".
Additional information:
Affected modules and functionalities:
dali/operators/video/reader/video_reader_decoder_op.cc:PlanBatch,AssignRuns,DecodeBatch,DecodeLanePart;DecodeSampleis the former per-sample body ofPrefetch, now on a lane's decoder, stream and constant frame;DecodeLanes and a thread pool (only with more than one lane);VideoSample::planned, for samples repeated bypad_last_batch;num_threadsdecoders, and memory growing with the number of decoders.dali/operators/video/reader/decode_lane_planner.{h,cc}(new):SampleFrames,DecoderPosition,CanReachWithoutSeek,PlanLanes. They are separate from the operator so that they can be tested on their own.dali/operators/video/frames_decoder_gpu.{h,cc}:FramesDecoderGpu::NumEngines(device_id).fn.experimental.decoders.video is not affected.
Key points relevant for the review:
SeekFrameseeks (CanReachWithoutSeek) and where each sample leaves its decoder (DecoderPosition,AddDecodeWork). A wrong prediction only makes the plan worse, never the output wrong: each sample is still decoded byDecodeSamplewith the decoder's own seeking logic, as before.PlanLanesgo to the lowest lane). Otherwise a decoder's history, and with it the output, could change from run to run.Tests:
decoder/test_video.py,checkpointing/test_dali_checkpointing.py: test_experimental_video_reader*,test_video_pipeline.pydecoder/test_video.py:test_reader_output_independent_of_num_threads,test_reader_batch_size_1_output_independent_of_num_threads,test_reader_multi_run_lanes_output_independent_of_num_threads(fewer lanes than samples), CPU and GPU, andtest_reader_lead_in_output_independent_of_num_threads(GPU only: H.264 clip with a long edit-list lead-in). Each compares the output with 1 and with N threads, and checks the frames against PyAV.test_reader_pad_last_batch_repeats_last_sample: a shard's last sample repeated in its batch and as the whole next batch.test_reader_reports_first_error_of_batch: samples fail in two lanes and in the next batch; the error is the batch's first one.decode_lane_planner_test.cc(new):DecodeLanePlannerTest.SampleFrames,CanReachWithoutSeek,LongestFirstToEarliestFinish,HeldFileContinues,NoIndex,HeldFileGoesFirst,RunsOfAFileInAscendingOrder,LeadIn,ResetAfterTheLastFrame,LaneOrder,PlannedPicturesAreDecoded.frame_index_cache_test.cc:FrameIndexCacheTest.HandBuiltIndexIsInDecodingOrder,FrameIndexCacheTest.KeepsDecodingOrder(an index without decoding order falls back to frame order; a cached index, and a decoder's copy of it, keep the decoding order that the planner uses).Checklist
Documentation
DALI team only
Requirements
REQ IDs: N/A
JIRA TASK: DALI-4935