@@ -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
984986torch::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)
0 commit comments