Repository navigation
Do not map, convert nor decode video pictures that are never read - #6562
Draft
jantonguirao wants to merge 5 commits into
Draft
jantonguirao wants to merge 5 commits into
jantonguirao wants to merge 5 commits into
Conversation
This was referenced Oct 27, 2026
jantonguirao
force-pushed
the
janton/dali-4935-unread-pictures
branch
2 times, most recently
from
October 30, 2026 17:07
9649584 to
bfe8c3e
Compare
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>
AddDecodeWork ignores frames out of range (a constant frame from HandleBoundary is copied, not decoded) instead of indexing past the index. PositionAt returns -1 after the last frame, as NextFrameIdx does, and also counts the leading pictures of an open GOP, decoded after its keyframe. BuildIndex does not ask the demuxer where seeks land in all-intra streams, where it would read the whole file again. The docs say what a decoded picture is on each decoder. Tests: exact estimates on hand-built indices (B-frames, a hidden lead-in, a seek landing before its keyframe, an open GOP, out-of-range frames, no decoding order); the reopenings are checked against a new Reopens() counter; the GPU slack only lets a decoder be ahead, not behind; the seek landing of files without hidden packets is checked against the demuxer; "across a keyframe" runs before the far jump, so that most files have a keyframe ahead. Signed-off-by: Joaquin Anton Guirao <janton@nvidia.com>
…r names 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>
jantonguirao
force-pushed
the
janton/dali-4935-unread-pictures
branch
from
October 30, 2026 17:11
bfe8c3e to
b4683c9
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:
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 as a whole. This PR covers one concern only: the GPU video decoder no longer maps, converts or keeps a frame buffer for pictures that are never read. It also skips decoding H.264 non-reference pictures that are never read.Depends on: #6561, which itself builds on #6556, #6559 and #6540. Until those are merged, their commits are also on this branch. This PR's own change is only the last commit, "Do not map, convert nor decode video pictures that are never read". The other commits belong to other PRs:
The problem
FramesDecoderGpuhas to decode every picture between a keyframe and the frames it returns, because later pictures may refer to them. Until now it also did everything else for each of those pictures. When a picture was not returned right away, it was mapped from the NVDEC output surface and color-converted into a frame buffer. That buffer was device memory the size of one output frame. Many of these pictures are never read with their data:stride > 1), and the frames between a keyframe and the target of a seek.For each such picture, the decoder mapped an NVDEC surface and launched a color conversion into a frame buffer, and then threw the result away. This used GPU time and device memory. With an edit-list lead-in, the frame buffer pool also grew to one buffer per hidden picture. This affects the GPU backends of
fn.experimental.readers.video,fn.experimental.decoders.videoandfn.experimental.inputs.video, which all useFramesDecoderGpu. The effect is largest for strided sampling, for seeks into long GOPs, and for videos with an edit-list lead-in.The change
The decoder now knows which frames it reads next (
FramesDecoderBase::frames_to_read_):DecodeFramessets it to the frames of the sample it has yet to read.SeekFramesets it to the frame it seeks to, unlessDecodeFrameshas already set it.FramesDecoderGpu::CanBeReadWithData(pts)decides whether a buffered frame can still be read. The index is sorted by timestamp (SetIndexnow asserts this). A frame can be read only if:DecodeFramescall on the same decoder may read it.Without an index, every frame can be read, as before.
In
HandlePictureDisplay, a buffered frame that cannot be read keeps its slot in the buffer, but only its timestamp is kept (BufferedFrame::has_data_ == false). It is neither mapped nor converted.Frame buffers are attached lazily (
AttachFrameBuffer). A slot reuses the buffer of an empty slot or of a timestamp-only slot. Only if there is none is a new buffer allocated. Neither parser initialization nor growing the buffer (FindEmptySlot) allocates a frame buffer for every slot any more.In
ProcessPictureDecode, an H.264 non-reference frame picture (ref_pic_flag == 0, not a field picture) that is not read with its data (IsPictureReadWithData) is not decoded at all. No other picture refers to it, so only its timestamp is kept for display order. These pictures are counted bySkippedPictures().FramesDecoderBase::ParsedPictures()(decoded plus skipped) is now the number that theAddDecodeWorkestimate from Estimate a video decoder's work from the decoding order in its index #6559 matches. Other codecs, and reference or field pictures, are always decoded.Reading a frame from the buffer that kept only its timestamp fails with an internal error, instead of returning a frame whose data was never written.
The frames returned do not change. The tests compare them against a decoder that maps every picture.
Additional information:
Affected modules and functionalities:
dali/operators/video/frames_decoder_base.{h,cc}:frames_to_read_, set byDecodeFramesImplandSeekFrame;skipped_pictures_andParsedPictures();SetIndexthat the index is sorted by timestamp.dali/operators/video/frames_decoder_gpu.{h,cc}:BufferedFrame::has_data_;CanBeReadWithData,IsPictureReadWithDataandAttachFrameBuffer;ProcessPictureDecode;ReadNextFrameWithIndex;MappedPictures(),SkippedPictures(),NumFrameBuffers()andSetBufferedFrameDataForTesting.dali/operators/video/frames_decoder_test.cc: new tests, and the decode-work checks now useParsedPictures().Users: the GPU backends of
fn.experimental.readers.video,fn.experimental.decoders.videoandfn.experimental.inputs.video. The CPU decoder is not changed.Key points relevant for the review:
CanBeReadWithDatamust never say "no" to a frame that is read later. If it did, that frame would have no data. This PR turns that case into an internal error (ReadNextFrameWithIndex), not a garbage frame. Please check the reasoning in its comment:next_frame_idx_.frames_to_read_are always kept.frames_to_read_lifetime. It is a span into a localSmallVectorinDecodeFramesImpl, and is cleared byAtScopeExit.SeekFrameonly sets it, and clears it, whenDecodeFrameshas not set it already.piped_pts_.front(), orcurrent_pts_when nothing is piped. This is the same timestampHandlePictureDisplaywill assign.IsPictureReadWithDatamakes the same decisionHandlePictureDisplaymakes: the picture is returned now (current_copy_to_output_), or buffered with its data. The skip is limited to H.264 frame pictures withref_pic_flag == 0.AttachFrameBufferbuffer reuse. It takes the buffer of an empty or timestamp-only slot. Any pending copy out of that buffer and the conversion into it both run onstream_, so they stay ordered.AddDecodeWork(from Estimate a video decoder's work from the decoding order in its index #6559) still counts skipped non-reference pictures as work. This is exact in parsed pictures (ParsedPictures), but it over-estimates NVDEC work. Weighting them would need a reference flag per index entry, so it is left for a follow-up.Tests:
New GTests in
frames_decoder_test.cc:FramesDecoderGpuTest.PicturesNotReadAreNotMappeduses a clip with 188 pictures hidden by an edit list. It reads frames in a row, with stride 3, and from frame 30. It checks that:FramesDecoderGpuTest.SameFramesAsMappingAllcovers Sintel clips, VFR H.264 and (when supported) HEVC and VFR HEVC. It compares the frames, and the total pictures parsed, with a decoder that maps every picture, for:REFLECT_101/CLAMP/CONSTANTpadding;AvSeekFrameto a frame that is not a keyframe.FramesDecoderGpuTest.ReadingFrameNotMappedFailschecks that reading a frame whose data was not kept raises the internal error and does not return a garbage frame.Changed: the decode-work helpers used by
FramesDecoderTest_CpuOnlyTests.DecodeWork,FramesDecoderGpuTest.DecodeWorkandFramesDecoderGpuTest.DecodeWorkFromPositionnow compare the estimate withParsedPictures().Existing tests that cover the change:
FramesDecoderGpuTest.*tests, e.g.ConstantFrameRate*,VariableFrameRate*,InMemory*,*NoIndex,RawH264andRawH265;dali/test/python/test_video_reader.py,dali/test/python/decoder/test_video.py,dali/test/python/input/test_video.pyanddali/test/python/test_video_pipeline.py.Checklist
Documentation
DALI team only
Requirements
REQ IDs: N/A
JIRA TASK: DALI-4935