Skip to content

Commit 758e067

Browse files
authored
Fix stream ordering issues in CUDA Blocks API (#1646)
1 parent 1648d13 commit 758e067

2 files changed

Lines changed: 60 additions & 36 deletions

File tree

src/torchcodec/_core/BetaCudaDeviceInterface.cpp

Lines changed: 55 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -792,12 +792,14 @@ int BetaCudaDeviceInterface::receive_frame(UniqueAVFrame& av_frame) {
792792
proc_params.progressive_frame = disp_info.progressive_frame;
793793
proc_params.top_field_first = disp_info.top_field_first;
794794
proc_params.unpaired_field = disp_info.repeat_first_field < 0;
795-
// We set the NVDEC stream to the current stream. It will be waited upon
796-
// by the color conversion stream before any color conversion.
795+
// We set the NVDEC stream to the current stream, and remember it: consumers
796+
// of the mapped surface run later and possibly on a different stream, so they
797+
// need to know which stream produces the surface's content in order to wait
798+
// on it.
797799
// Re types: we get a cudaStream_t from PyTorch but it's interchangeable with
798800
// CUstream
799-
proc_params.output_stream =
800-
reinterpret_cast<CUstream>(get_current_cuda_stream(device_.index()));
801+
nvdec_output_stream_ = get_current_cuda_stream(device_.index());
802+
proc_params.output_stream = reinterpret_cast<CUstream>(nvdec_output_stream_);
801803

802804
CUdeviceptr frame_ptr = 0;
803805
unsigned int pitch = 0;
@@ -948,9 +950,9 @@ void BetaCudaDeviceInterface::make_frame_standalone(UniqueAVFrame& av_frame) {
948950
// receive_frame() without losing the data.
949951
// - CPU-fallback frames are uploaded here too, so that a PacketDecoder always
950952
// hands out frames that live on its own device.
951-
// The copy of GPU frames and the upload of CPU frames is async: we thus
952-
// record the producer stream in the attached data so that the stream can be
953-
// waited upon before running the color-conversion.
953+
// The copy of GPU frames is async, so we record the stream it runs on in the
954+
// attached data: a ColorConverter on another stream must wait on it before
955+
// reading the frame.
954956
STD_TORCH_CHECK(
955957
mode() == Mode::DecoderOnly,
956958
"make_frame_standalone() is only valid in decoder-only mode: standalone "
@@ -960,7 +962,7 @@ void BetaCudaDeviceInterface::make_frame_standalone(UniqueAVFrame& av_frame) {
960962

961963
torch::stable::Tensor storage;
962964
if (decoding_on_cpu_) {
963-
auto uploaded = upload_cpu_frame_to_gpu_on_current_stream(*av_frame);
965+
auto uploaded = upload_cpu_frame_to_gpu(*av_frame, current_stream);
964966
av_frame = std::move(uploaded.av_frame);
965967
storage = std::move(uploaded.storage);
966968
} else {
@@ -983,7 +985,7 @@ void BetaCudaDeviceInterface::make_frame_standalone(UniqueAVFrame& av_frame) {
983985

984986
torch::stable::Tensor BetaCudaDeviceInterface::copy_nvdec_surface(
985987
UniqueAVFrame& av_frame,
986-
cudaStream_t stream) {
988+
cudaStream_t current_stream) {
987989
// The amount of bytes an NV12 image takes is:
988990
// num_bytes = len(Y) + len(UV)
989991
// = num_pixels + num_pixels / 2
@@ -1003,16 +1005,21 @@ torch::stable::Tensor BetaCudaDeviceInterface::copy_nvdec_surface(
10031005
auto storage =
10041006
torch::stable::empty({num_bytes}, kStableUInt8, std::nullopt, device_);
10051007

1006-
// TODO_API_BREAKDOWN CORRECTNESS P1: I suspect we don't need to wait on the
1007-
// nvdec stream here, because we can only arrive here from a path where the
1008-
// frame has already been mapped so its data is available - worth double
1009-
// checking.
1008+
// The surface's content is produced by the mapping post-processing that
1009+
// receive_frame() enqueued on nvdec_output_stream_, which isn't necessarily
1010+
// the stream we're copying on.
1011+
if (current_stream != nvdec_output_stream_) {
1012+
sync_streams(
1013+
/*running_stream=*/nvdec_output_stream_,
1014+
/*waiting_stream=*/current_stream);
1015+
}
1016+
10101017
cudaError_t err = cudaMemcpyAsync(
10111018
storage.mutable_data_ptr(),
10121019
av_frame->data[0],
10131020
static_cast<size_t>(num_bytes),
10141021
cudaMemcpyDeviceToDevice,
1015-
stream);
1022+
current_stream);
10161023
STD_TORCH_CHECK(
10171024
err == cudaSuccess,
10181025
"Failed to copy NVDEC surface: ",
@@ -1064,17 +1071,17 @@ void BetaCudaDeviceInterface::flush() {
10641071
send_seqhdr_packet();
10651072
}
10661073

1067-
GpuFrameAndStorage
1068-
BetaCudaDeviceInterface::upload_cpu_frame_to_gpu_on_current_stream(
1069-
const AVFrame& cpu_frame) {
1074+
GpuFrameAndStorage BetaCudaDeviceInterface::upload_cpu_frame_to_gpu(
1075+
const AVFrame& cpu_frame,
1076+
cudaStream_t stream) {
10701077
// This is called in the context of the CPU fallback: the frame was decoded
10711078
// on the CPU, and in this function we convert that frame into a format we
10721079
// can color-convert on the GPU, and send it there.
10731080
// We do that in 2 steps:
10741081
// - First we convert the input CPU frame into an intermediate CPU frame in
10751082
// the target format using sws_scale.
1076-
// - Then we allocate GPU memory and copy the CPU frame to the GPU
1077-
// asynchronously on the current stream.
1083+
// - Then we allocate GPU memory and copy the CPU frame to the GPU on the
1084+
// given stream.
10781085
// We return the new AVFrame and its associated GPU storage so that the caller
10791086
// can handle the memory lifetime. The GPU storage is a torch
10801087
// Tensor because we want to rely on the torch CUDA allocator.
@@ -1200,17 +1207,18 @@ BetaCudaDeviceInterface::upload_cpu_frame_to_gpu_on_current_stream(
12001207
gpu_frame->data[p] = storage_ptr + plane_offsets[p];
12011208
gpu_frame->linesize[p] = row_bytes;
12021209

1203-
// Note that we use cudaMemcpy2D here instead of cudaMemcpy because the
1204-
// linesizes (strides) may be different than the widths for the input CPU
1205-
// frame. That's precisely what cudaMemcpy2D is for.
1206-
cudaError_t err = cudaMemcpy2D(
1210+
// Note that we use cudaMemcpy2DAsync here instead of cudaMemcpyAsync
1211+
// because the linesizes (strides) may be different than the widths for the
1212+
// input CPU frame. That's precisely what the 2D variants are for.
1213+
cudaError_t err = cudaMemcpy2DAsync(
12071214
gpu_frame->data[p],
12081215
gpu_frame->linesize[p],
12091216
intermediate_cpu_frame->data[p],
12101217
intermediate_cpu_frame->linesize[p],
12111218
row_bytes,
12121219
plane_heights[p],
1213-
cudaMemcpyHostToDevice);
1220+
cudaMemcpyHostToDevice,
1221+
stream);
12141222
STD_TORCH_CHECK(
12151223
err == cudaSuccess,
12161224
"Failed to copy plane ",
@@ -1219,6 +1227,17 @@ BetaCudaDeviceInterface::upload_cpu_frame_to_gpu_on_current_stream(
12191227
cudaGetErrorString(err));
12201228
}
12211229

1230+
// intermediate_cpu_frame is freed when this function returns, so we can't
1231+
// leave the copies in flight, we must wait for it to finish. Making the
1232+
// upload truly async would mean allocating the intermediate frame in pinned
1233+
// memory and keeping it alive until the copies complete. Probably not worth
1234+
// it for this CPU fallback path that is already slow by nature.
1235+
cudaError_t err = cudaStreamSynchronize(stream);
1236+
STD_TORCH_CHECK(
1237+
err == cudaSuccess,
1238+
"Failed to wait for the CPU-to-GPU upload: ",
1239+
cudaGetErrorString(err));
1240+
12221241
ret = av_frame_copy_props(gpu_frame.get(), &cpu_frame);
12231242
STD_TORCH_CHECK(
12241243
ret >= 0,
@@ -1233,9 +1252,10 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
12331252
FrameOutput& frame_output,
12341253
std::optional<torch::stable::Tensor> pre_allocated_output_tensor) {
12351254
CudaContextGuard context_guard(device_.index());
1255+
cudaStream_t current_stream = get_current_cuda_stream(device_.index());
12361256

1237-
// Capture original dimensions before
1238-
// upload_cpu_frame_to_gpu_on_current_stream() may round them up to even.
1257+
// Capture original dimensions before upload_cpu_frame_to_gpu() may round them
1258+
// up to even.
12391259
FrameDims original_dims(av_frame.height, av_frame.width);
12401260

12411261
// We may need to upload a frame here in case of the CPU fallback. This is
@@ -1262,7 +1282,7 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
12621282
// the color conversion below.
12631283
GpuFrameAndStorage uploaded;
12641284
if (needs_upload) {
1265-
uploaded = upload_cpu_frame_to_gpu_on_current_stream(av_frame);
1285+
uploaded = upload_cpu_frame_to_gpu(av_frame, current_stream);
12661286
}
12671287
const AVFrame& gpu_frame = needs_upload ? *uploaded.av_frame : av_frame;
12681288

@@ -1282,13 +1302,14 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
12821302
gpu_frame.opaque_ref->data);
12831303
producer_stream = attached_data->producer_stream;
12841304
} else {
1285-
// In case of CPU fallback, the producer stream is indeed the current
1286-
// stream.
1287-
// TODO_API_BREAKDOWN CORRECTNESS P1: when we're not in CPU fallback, what
1288-
// is the producer stream? It's the NVDEC stream isn't it? I think it works
1289-
// because we know the data is valid since we mapped the frame, but we might
1290-
// want to document this
1291-
producer_stream = get_current_cuda_stream(device_.index());
1305+
STD_TORCH_CHECK(
1306+
mode() == Mode::Both,
1307+
"Color conversion requires the interface to be initialized for color "
1308+
"conversion.");
1309+
// Either the frame was just uploaded on the current stream, or it's a
1310+
// mapped NVDEC surface, whose content is produced by the mapping
1311+
// post-processing that receive_frame() enqueued on nvdec_output_stream_.
1312+
producer_stream = needs_upload ? current_stream : nvdec_output_stream_;
12921313
}
12931314

12941315
auto convert_frame = [&](std::optional<torch::stable::Tensor> pre_alloc)

src/torchcodec/_core/BetaCudaDeviceInterface.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,18 @@ class BetaCudaDeviceInterface : public DeviceInterface {
103103
CUdeviceptr previously_mapped_frame_ = 0;
104104
void unmap_previous_frame();
105105

106+
cudaStream_t nvdec_output_stream_ = nullptr;
107+
106108
UniqueAVFrame convert_cuda_frame_to_av_frame(
107109
CUdeviceptr frame_ptr,
108110
unsigned int pitch,
109111
const CUVIDPARSERDISPINFO& disp_info);
110112

111113
void make_frame_standalone(UniqueAVFrame& av_frame) override;
112114

113-
GpuFrameAndStorage upload_cpu_frame_to_gpu_on_current_stream(
114-
const AVFrame& cpu_frame);
115+
GpuFrameAndStorage upload_cpu_frame_to_gpu(
116+
const AVFrame& cpu_frame,
117+
cudaStream_t stream);
115118

116119
torch::stable::Tensor copy_nvdec_surface(
117120
UniqueAVFrame& av_frame,

0 commit comments

Comments
 (0)