Repository navigation
Sync the stream before a GPU video decoder frees its frame buffers - #6556
jantonguirao merged 2 commits into
Conversation
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>
There was a problem hiding this comment.
🟢 Approval recommended
The synchronization precedes frame-buffer destruction and addresses the identified lifetime race, with no blocking issues found in static review.
No open issues 🎉
What changed in this PR
This focused fix prevents GPU video frame buffers from returning to the memory pool while asynchronous copies still use them. It addresses the existing decoder lifetime race independently of the parallel decoding changes.
Changes:
- Synchronize the decoder’s CUDA stream before releasing frame buffers.
- Clear synchronization errors without throwing from the destructor.
| File | Description |
|---|---|
dali/operators/video/frames_decoder_gpu.cc |
Wait for pending stream work before decoder members are destroyed. |
Review effort: Balanced
[Medium impact] The change reaches GPU video decoding workflows that use this decoder. Restore non-throwing cleanup before merging so a CUDA error cannot abort the application during decoder destruction. Findings
|
| if (cudaStreamSynchronize(stream_) != cudaSuccess) | ||
| (void)cudaGetLastError(); |
There was a problem hiding this comment.
Nitpick: shouldn't this be CUDA_DTOR_CALL?
| // for the copies out of it (CopyFrame, copyD2D) that may still be pending on stream_. Nothing | ||
| // is thrown from the destructor: a failing sync means the CUDA context is already broken, and | ||
| // that error is reported by the call that caused it. | ||
| if (cudaStreamSynchronize(stream_) != cudaSuccess) |
There was a problem hiding this comment.
What kind of error is acceptable here, except the "unloading" one already swallowed by CUDA_DTOR_CALL?
There was a problem hiding this comment.
None that should be hidden. Apart from unloading, the sync can only fail here with a sticky error from earlier work on the stream, and then the context is unusable anyway. I had swallowed it to avoid throwing from a destructor, but DALI checks CUDA calls in destructors with CUDA_DTOR_CALL elsewhere (e.g. cudaStreamDestroy and cudaEventDestroy in CUDAStream/CUDAEvent), so I switched to it in aec9d1a.
…_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>
|
!build |
| FramesDecoderGpu::~FramesDecoderGpu() { | ||
| // frame_buffer_ goes back to the memory pool right after this, without stream ordering: wait | ||
| // for the copies out of it (CopyFrame, copyD2D) that may still be pending on stream_ | ||
| CUDA_DTOR_CALL(cudaStreamSynchronize(stream_)); |
There was a problem hiding this comment.
GPU errors abort the application
[Bug] CUDA_DTOR_CALL throws for every CUDA failure except runtime unloading. If cudaStreamSynchronize reports an earlier GPU failure during decoder cleanup, that exception leaves the non-throwing FramesDecoderGpu destructor and calls std::terminate. The host process aborts instead of reporting the pipeline error.
Restore the previous non-throwing check so cleanup clears the CUDA error without throwing.
| CUDA_DTOR_CALL(cudaStreamSynchronize(stream_)); | |
| if (cudaStreamSynchronize(stream_) != cudaSuccess) | |
| (void)cudaGetLastError(); |
There was a problem hiding this comment.
We are OK with that
|
CI MESSAGE: [77044862]: BUILD STARTED |
|
CI MESSAGE: [77044862]: BUILD PASSED |
Category:
Bug fix (non-breaking change which fixes an issue)
Description:
This PR is split out of #6545, which was too large to review as one change. It is based directly on
mainand does not depend on the other PRs of the split.Problem.
FramesDecoderGpukeeps its decoded frames inframe_buffer_(DeviceBuffers), and copies frames out of it into the output with asynchronous device-to-device copies (copyD2D) enqueued on the decoder's stream. When the decoder is destroyed,frame_buffer_is returned to the memory pool with a plain (not stream-ordered) deallocation, while one of those copies may still be pending on the stream. The memory can then be handed out to work on another stream and overwritten before the pending copy has read it, so the copied frame may contain wrong data. Nothing reports this: the result is silently corrupted output, not an error.When it happens.
fn.experimental.readers.video(GPU) replaces its decoder inPrefetchevery time the next sample comes from another file, so a decoder is destroyed right after the last copy out of it has been enqueued, which makes the window real. The same destructor runs for every other user ofFramesDecoderGpu:fn.experimental.decoders.videoandfn.experimental.inputs.video(mixed backend). The decode lanes of #6545 (#6563, #6564) run several GPU decoders concurrently, which makes the race more likely, but it exists onmainalready.Fix. The destructor of
FramesDecoderGpunow callscudaStreamSynchronize(stream_)before its members (includingframe_buffer_) are released, so every copy out of the frame buffers has finished when their memory goes back to the pool. The call is checked withCUDA_DTOR_CALL, as other CUDA calls in DALI destructors: only the error of an unloading CUDA runtime is ignored.Additional information:
Affected modules and functionalities:
dali/operators/video/frames_decoder_gpu.cc:FramesDecoderGpu::~FramesDecoderGpu()synchronizesstream_before the frame buffers are freed.Key points relevant for the review:
frame_buffer_.cudaStreamSynchronizein the destructor is checked withCUDA_DTOR_CALL. Apart from an unloading runtime, it can only fail with a sticky error from earlier work on the stream, when the CUDA context is unusable anyway.Tests:
frames_decoder_test.cc:FramesDecoderGpuTest.*(decoders created and destroyed on a stream)video_reader_decoder_op_test.cc:VideoReaderDecoderGpuTest.*,VideoReaderDecoderCompareTest.CompareReaderstest_video_reader.py:test_compare_experimental_to_legacy_reader_*,test_uniform_samplewithdevice="gpu"(several files per reader, so decoders are destroyed while reading)decoder/test_video.py:fn.experimental.decoders.videotests withdevice="mixed"input/test_video.py:test_video_input_*withdevice="mixed"Checklist
Documentation
DALI team only
Requirements
REQ IDs: N/A
JIRA TASK: DALI-4935