Repository navigation
Keep several NVDEC pictures in flight per decoder - #6606
Conversation
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues are identified for the GPU decoder changes.
No open issues 🎉
What changed in this PR
This PR enables multiple pictures to remain in flight in FramesDecoderGpu, reducing NVDEC idle time for GPU video readers, decoders, and inputs.
Changes:
- Moves frame mapping and conversion to the parser’s display callback, with a display delay of one and additional decode surfaces.
- Tracks timestamps and pending display state per surface to preserve seek behavior and handle delayed frames at end of stream.
- Adds GPU regression tests for HEVC open-GOP seeks and VP8/VP9 last-frame reads.
| File | Description |
|---|---|
dali/operators/video/frames_decoder_test.cc |
Adds HEVC seek and VP8/VP9 end-of-stream regression tests. |
dali/operators/video/frames_decoder_gpu.h |
Declares display delay, surface-count helper, and per-surface tracking state. |
dali/operators/video/frames_decoder_gpu.cc |
Implements delayed display, expanded surface allocation, timestamp tracking, and EOF/flush handling. |
Review effort: Balanced
[Medium impact] The PR appears safe to merge; no new actionable issues were found.
|
74817aa to
d75d154
Compare
| // keeps that many more decoded pictures in the parser, and the picture being mapped and | ||
| // converted must not be decoded over either: two spare surfaces, as the minimum does not | ||
| // guarantee the best performance (nvcuvid.h) | ||
| return video_format.min_num_decode_surfaces + kDisplayDelay + 2; |
There was a problem hiding this comment.
please use const var instead of magic number
There was a problem hiding this comment.
Done: the 2 is now kSpareDecodeSurfaces, declared next to kDisplayDelay in the header with the reason for it.
The GPU video decoder mapped and converted every picture right after submitting it, inside the parser's decode callback: NVDEC decoded one picture at a time and sat idle while the host waited for the picture, converted it and parsed the next packets. The pictures are now mapped by the parser's display callback, in display order, with a display delay of one picture and two spare decode surfaces, so that NVDEC keeps decoding the next pictures meanwhile. The pts of each picture is still taken from the packets in decode order and kept with its surface: the timestamps that the parser reports to the display callback are not those of the packets when it drops the leading pictures of an HEVC CRA picture after a seek. The second field of a field pair shares the surface and the frame of the first one, so it does not take another pts. With the display delay, the parser keeps the last decoded picture until the end of the stream. VP8 and VP9 pictures are decoded while their own packet is parsed, so at EOF there may be no packet left whose picture was not decoded, while a picture still waits for the display callback. The end of stream check now also counts those pictures (it failed with 'Assert on "piped_pts_.size() >= 1" failed' when reading the last frame of a VP8/VP9 file), and the pending flags are cleared after the end of stream packet. Tests: HevcSeekToOpenGopKeyframes reads shuffled seeks to the keyframes of an open-GOP HEVC stream and requires the frames of a sequential read. ReadsTheLastFrameOfCodecsWithoutDecodeLag reads VP8 and VP9 files to the end, sequentially and after seeks, and compares every frame with the CPU decoder. Signed-off-by: Joaquin Anton Guirao <janton@nvidia.com>
d75d154 to
a6dda46
Compare
Signed-off-by: Joaquin Anton Guirao <janton@nvidia.com>
|
!build |
|
CI MESSAGE: [79539833]: BUILD STARTED |
|
CI MESSAGE: [79539833]: BUILD FAILED |
The GPU decoder takes pictures from the parser's display callback with a display delay, so it is ahead of the AddDecodeWork estimate by the stream's reordering depth, plus kDisplayDelay, plus a packet of lag. The lead is there from the first frame after a start and only shrinks near the end of the stream. The test allowed a slack of one picture, which was enough for the decoder without the display callback, so FramesDecoderGpuTest.DecodeWork failed for every clip. Bound the lead as a number of frames of lookahead in display order: the lag, the reordering depth of the codec and kDisplayDelay. Track the real lead between the samples instead of clamping the surplus to the slack, so that it can shrink at the end of the stream. The CPU decoder keeps its exact check. Signed-off-by: Joaquin Anton Guirao <janton@nvidia.com>
|
!build |
|
CI MESSAGE: [79818830]: BUILD STARTED |
|
CI MESSAGE: [79818830]: BUILD FAILED |
|
CI MESSAGE: [79818830]: BUILD PASSED |
Category:
New feature (non-breaking change which adds functionality)
Description:
Problem.
FramesDecoderGpukept exactly one picture in flight per decoder. The parser was created withulMaxDisplayDelay = 0and no display callback. Every decode callback submitted the picture withcuvidDecodePictureand then, inline, mapped it (cuvidMapVideoFrameblocks until that picture is decoded), converted it to RGB, synchronized the stream and unmapped it. Only then did the next packet get read, filtered and parsed. NVDEC was idle for the whole map wait, conversion, sync and host-side packet handling. That cost is per picture, so it limits every single-stream read.Mechanism. Pictures are now mapped in display order by the parser's display callback (
pfnDisplayPicture), with a display delay of 1 (kDisplayDelay):min_num_decode_surfaces + kDisplayDelay + 2surfaces. The display delay keeps that many more pictures, and the picture being mapped and converted must not be decoded over. The NVDEC cache key already includes the surface count.CurrPicIdx). The display callback looks the pts up bypicture_indexand does the existing map, convert, sync and unmap.flush_turns the display callback into a no-op, so pictures that the parser releases while flushing on a seek or reset are dropped, as before.Pitfall 1: open-GOP HEVC timestamps. The obvious implementation takes the pts from
CUVIDPARSERDISPINFO::timestamp. That is wrong after a seek into an HEVC open GOP:That variant passed every H.264 check and gave wrong frames on shuffled HEVC reads. The pts is therefore taken, as before, from the decode-order queue of sent packets at decode time, and kept per surface.
New gtest
FramesDecoderGpuTest.HevcSeekToOpenGopKeyframesreads the HEVC Sintel trailer sequentially and with shuffled seeks to its keyframes, and requires exact equality. It passes with this change and fails on a build that uses the display timestamp.Pitfall 2: the last picture of a VP8/VP9 stream. VP8/VP9 pictures are decoded while their own packet is parsed. H.264/HEVC instead have a one-packet lag. So at EOF the queue of sent packets can be empty while the last picture still waits in the parser for its display callback (the display delay). The EOF check
DALI_ENFORCE(piped_pts_.size() >= 1)then failed whenever a sample read the last frame of a VP8/VP9 file. The decoder now tracks pictures that were decoded but not displayed yet (surface_to_display_, cleared on flush), and the EOF check accepts those.New gtest
FramesDecoderGpuTest.ReadsTheLastFrameOfCodecsWithoutDecodeLagcovers VP8 and VP9: it reads sequentially to the end and seeks near the end. It fails with the original assertion on the build without this fix and passes with it.Measured gains (an H100,
fn.experimental.readers.videoon the GPU, 720p Sintel trailer):main(4 rounds)Protocol:
mainbuild measured as a second arm) bounds the noise at 0.3% or less.mainon all benchmark groups (sequential, random, multi-stream, reader depth, CPU) at 1, 2, 4 and 8 threads.The timings were taken on an earlier revision of this change. What was added since does not change the work per picture of a progressive stream: the end-of-stream check and the clearing of the pending flags, which run once per stream end, the skipped pts pop for the second field of a field pair, and the tests.
Mechanism, from one Nsight Systems capture per arm (H.264, 4 threads):
main, 7 (p10-p90 range 4-10) with this change.cuvidDecodePicturecalls: 788 us onmain, 699 us with this change.Measured on a second GPU (an RTX 5080,
fn.experimental.readers.videoon the GPU, 720p Sintel trailer clips; builds of this change and of its base commit that differ only inlibdali_operators.so):Correctness checks (an H100; every comparison was made against
mainbuilt from the same base on the same machine):*FramesDecoder*,*FrameIndexCache*,*VideoReader*,*VideoDecoder*,*VideoInput*, one process per test: 101 run.VideoReaderTest_{readers__Video,experimental__readers__Video}.MultipleVideoResolution, which fail onmainon the same machine too.mainand on this branch:decoder/test_video.py: 148 OK, 10 skipped.input/test_video.py: 42 OK.test_video_reader.py: 60 OK.test_video_pipeline.py: 65 OK.test_video_reader_resize.py: 14 OK.test_video_reader,test_video_reader_resize_readerandtest_experimental_video_reader: 124 OK.readers.videoGPU, shuffled sequences, checked against PyAV per frame:mainon every H.264, HEVC, VP8, VP9 and MPEG-4 clip of the test data, including the AVI/MKV containers.mainon every frame.mainis not deterministic run to run on that clip either.fn.decoders.videoandfn.experimental.inputs.videoon the GPU, over 8 clips (H.264, HEVC, VP8, VP9, MPEG-4, VFR) plusstart_frame/sequence_lengthreads of the long H.264, HEVC and HEVC 10-bit clips: all 22 outputs byte-identical tomain.Limitations:
show_existing_frametemporal units, which have no decode callback. The pts recorded per surface from the decode-order packet queue assumes one decoded picture per packet, so timestamps of AV1 streams with such frames may be off. (Onmain, those frames were never displayed and hidden frames were.) This was checked on a Blackwell GPU with AV1 decode: the reader already returns wrongly indexed AV1 frames onmain, because a temporal unit does not map to one decoded picture (it can hold hidden frames, or only ashow_existing_frame), and this change neither fixes nor worsens that. Tracked in DALI-5008.Additional information:
Affected modules and functionalities:
dali/operators/video/frames_decoder_gpu.{h,cc}:dali/operators/video/frames_decoder_test.cc: two new GPU tests, and the slack ofFramesDecoderGpuTest.DecodeWork: the decoder is now ahead of theAddDecodeWorkestimate by the reordering depth of the stream pluskDisplayDelay, plus a packet of lag, so the test bounds that lead (as a number of frames of lookahead) instead of a single picture. The CPU decoder keeps the exact check.FramesDecoderGpu: the GPU video readers,fn.decoders.videoandfn.experimental.inputs.video.Key points relevant for the review:
CUVIDPARSERDISPINFO::timestamp. See pitfall 1.NumDecodeSurfaces), returned from the sequence callback and used for the cached decoder.DecodeWork: the reordering depth is chosen by codec (2 for H.264 and HEVC, 0 otherwise), and the constants were measured, not derived from the parser; several clips meet the bound exactly.AddDecodeWorkstill estimates the pictures up to the last frame read, so for the GPU decoder it is a lower bound. Only tests call it andDecodedPictures()in this tree.Tests:
Checklist
Documentation
DALI team only
Requirements
REQ IDs: N/A
JIRA TASK: DALI-4946