Skip to content

Sync the stream before a GPU video decoder frees its frame buffers - #6556

Merged
jantonguirao merged 2 commits into
NVIDIA:mainfrom
jantonguirao:janton/dali-4935-gpu-decoder-free-sync
Oct 27, 2026
Merged

jantonguirao merged 2 commits into
NVIDIA:mainfrom
jantonguirao:janton/dali-4935-gpu-decoder-free-sync

Conversation

@jantonguirao

@jantonguirao jantonguirao commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

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 main and does not depend on the other PRs of the split.

Problem. FramesDecoderGpu keeps its decoded frames in frame_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 in Prefetch every 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 of FramesDecoderGpu: fn.experimental.decoders.video and fn.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 on main already.

Fix. The destructor of FramesDecoderGpu now calls cudaStreamSynchronize(stream_) before its members (including frame_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 with CUDA_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() synchronizes stream_ before the frame buffers are freed.

Key points relevant for the review:

  • The cost is one stream synchronization per decoder destruction (e.g. one per file switch in the reader), not per frame. It waits for all work enqueued on the decoder's stream so far (the stream the operator passed to the decoder), not only for the copies out of frame_buffer_.
  • cudaStreamSynchronize in the destructor is checked with CUDA_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:

  • Existing tests apply
    • frames_decoder_test.cc: FramesDecoderGpuTest.* (decoders created and destroyed on a stream)
    • video_reader_decoder_op_test.cc: VideoReaderDecoderGpuTest.*, VideoReaderDecoderCompareTest.CompareReaders
    • test_video_reader.py: test_compare_experimental_to_legacy_reader_*, test_uniform_sample with device="gpu" (several files per reader, so decoders are destroyed while reading)
    • decoder/test_video.py: fn.experimental.decoders.video tests with device="mixed"
    • input/test_video.py: test_video_input_* with device="mixed"
  • New tests added
    • Python tests
    • GTests
    • Benchmark
    • Other
  • N/A

Checklist

Documentation

  • Existing documentation applies
  • Documentation updated
    • Docstring
    • Doxygen
    • RST
    • Jupyter
    • Other
  • N/A

DALI team only

Requirements

  • Implements new requirements
  • Affects existing requirements
  • N/A

REQ IDs: N/A

JIRA TASK: DALI-4935

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>
Copilot AI balanced review requested due to automatic review settings October 27, 2026 15:37
@copy-pr-bot

copy-pr-bot Bot commented Oct 27, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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

@greptile-apps

greptile-apps Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[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

  1. P1 GPU errors abort the application ▶

Summary

The PR waits for the decoder's stream before returning its frame buffers to the memory pool.

  • GPU video decoders finish queued copies before releasing frame buffers.

Reviews (2) · Last reviewed commit: "Check the stream sync in the GPU video d..." · Reviewed by Greptile

Comment on lines +908 to +909
if (cudaStreamSynchronize(stream_) != cudaSuccess)
(void)cudaGetLastError();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: shouldn't this be CUDA_DTOR_CALL?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, done in aec9d1a.

// 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)

@mzient mzient Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of error is acceptable here, except the "unloading" one already swallowed by CUDA_DTOR_CALL?

@jantonguirao jantonguirao Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@jantonguirao

Copy link
Copy Markdown
Collaborator Author

!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_));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
CUDA_DTOR_CALL(cudaStreamSynchronize(stream_));
if (cudaStreamSynchronize(stream_) != cudaSuccess)
(void)cudaGetLastError();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are OK with that

@dali-automaton

Copy link
Copy Markdown
Collaborator

CI MESSAGE: [77044862]: BUILD STARTED

@dali-automaton

Copy link
Copy Markdown
Collaborator

CI MESSAGE: [77044862]: BUILD PASSED

@jantonguirao
jantonguirao merged commit 78e45ee into NVIDIA:main Oct 27, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants