@@ -526,11 +526,22 @@ BetaCudaDeviceInterface::~BetaCudaDeviceInterface() {
526526 // What happens to those decode surfaces that haven't yet been mapped is
527527 // unclear.
528528 flush ();
529+ if (surface_read_done_ != nullptr ) {
530+ // Whoever picks this decoder up from the cache will map its output
531+ // surface and overwrite it. We block the host until the last consumer is
532+ // done reading.
533+ cudaEventSynchronize (surface_read_done_);
534+ }
529535 unmap_previous_frame ();
530536 NVDECCache::get_cache (device_).return_decoder (
531537 &video_format_, surface_format_, std::move (decoder_));
532538 }
533539
540+ if (surface_read_done_ != nullptr ) {
541+ cudaEventDestroy (surface_read_done_);
542+ surface_read_done_ = nullptr ;
543+ }
544+
534545 if (video_parser_) {
535546 cuvidDestroyVideoParser (video_parser_);
536547 video_parser_ = nullptr ;
@@ -814,13 +825,15 @@ int BetaCudaDeviceInterface::receive_frame(UniqueAVFrame& av_frame) {
814825 // immediately after mapping them: they do the color-conversion in-between,
815826 // which involves a copy of the data, so that works.
816827 // We, OTOH, will do the color-conversion later, outside of receive_frame().
817- // So we unmap here: just before mapping a new frame. At that point we know
818- // that the previously-mapped frame is no longer needed:
828+ // So we unmap here: just before mapping a new frame. At that point the
829+ // previously-mapped frame has been consumed, or at least its consumption has
830+ // been enqueued:
819831 // - With SingleStreamDecoder, that frame was either color-converted (with a
820832 // copy), or that's a frame that was discarded in SingleStreamDecoder.
821- // Either way, the underlying output surface can be safely re-used.
822833 // - With the "Blocks" APIs, the PacketDecoder forces a copy in
823834 // make_frame_standalone().
835+ // Those reads are asynchronous, which is what the call below accounts for.
836+ order_mapping_after_surface_read ();
824837 unmap_previous_frame ();
825838 CUresult result = cuvidMapVideoFrame (
826839 *decoder_.get (),
@@ -838,6 +851,41 @@ int BetaCudaDeviceInterface::receive_frame(UniqueAVFrame& av_frame) {
838851 return AVSUCCESS ;
839852}
840853
854+ void BetaCudaDeviceInterface::record_surface_read (cudaStream_t stream) {
855+ // Called by every consumer of the mapped surface, once its read has been
856+ // enqueued on `stream`.
857+ // This sets the surface_read_done_ event that must be waited upon before
858+ // mapping a new frame on the surface.
859+ if (surface_read_done_ == nullptr ) {
860+ cudaError_t err =
861+ cudaEventCreateWithFlags (&surface_read_done_, cudaEventDisableTiming);
862+ STD_TORCH_CHECK (
863+ err == cudaSuccess,
864+ " cudaEventCreateWithFlags failed: " ,
865+ cudaGetErrorString (err));
866+ }
867+ cudaError_t err = cudaEventRecord (surface_read_done_, stream);
868+ STD_TORCH_CHECK (
869+ err == cudaSuccess, " cudaEventRecord failed: " , cudaGetErrorString (err));
870+ surface_reader_stream_ = stream;
871+ }
872+
873+ void BetaCudaDeviceInterface::order_mapping_after_surface_read () {
874+ // The mapping we're about to do on the NVDEC stream writes the output
875+ // surface, which the previous frame's consumer may still be reading from
876+ // another stream: the NVDEC stream must also wait on that consumer.
877+ if (surface_read_done_ == nullptr ||
878+ surface_reader_stream_ == nvdec_output_stream_) {
879+ return ;
880+ }
881+ cudaError_t err =
882+ cudaStreamWaitEvent (nvdec_output_stream_, surface_read_done_, 0 );
883+ STD_TORCH_CHECK (
884+ err == cudaSuccess,
885+ " cudaStreamWaitEvent failed: " ,
886+ cudaGetErrorString (err));
887+ }
888+
841889void BetaCudaDeviceInterface::unmap_previous_frame () {
842890 if (previously_mapped_frame_ == 0 ) {
843891 return ;
@@ -969,6 +1017,32 @@ void BetaCudaDeviceInterface::make_frame_standalone(UniqueAVFrame& av_frame) {
9691017 storage = copy_nvdec_surface (av_frame, current_stream);
9701018 }
9711019
1020+ // TODO_API_BREAKDOWN CORRECTNESS P0: `storage` comes from the PyTorch
1021+ // caching allocator on `current_stream`, but a ColorConverter reads it from
1022+ // whatever stream it runs on. The allocator only tracks the allocating
1023+ // stream: once the frame is dropped, the block returns to `current_stream`'s
1024+ // pool with no synchronisation, and the next frame's copy - same size, same
1025+ // stream - lands right in it while the converter is still reading. Frames
1026+ // are then silently corrupted whenever the converter lags behind the
1027+ // decoder, which is the normal state of a two-stream pipeline. Measured on a
1028+ // 4K clip with the converter backlogged: 117 of 119 frames wrong, and clean
1029+ // again as soon as the frames are kept alive.
1030+ // The usual remedy is Tensor::record_stream() on the consumer side, but
1031+ // neither the stable ABI nor the AOTI shim exposes it, and StableIValue has
1032+ // no Stream conversion, so it isn't reachable through the dispatcher either.
1033+ // We don't actually need it though: the allocator recycles the block because
1034+ // *we* drop our reference too early, so it's enough to hold on to the
1035+ // storage until the consumer is done. Have the ColorConverter record an
1036+ // event on its own stream into the attached data, and make
1037+ // standalone_frame_free_callback() hand (storage, event) to a per-device
1038+ // pending-release list instead of dropping the tensor. Drain that list
1039+ // opportunistically with cudaEventQuery. No host stall, and a consumer that
1040+ // permanently lags shows up as a growing list, i.e. as backpressure rather
1041+ // than as silent corruption.
1042+ // Frames that were never converted have no event and can be released
1043+ // straight away. Raw planes handed to the user via materialize() keep the
1044+ // storage alive on their own, and once the user drops those, ordering their
1045+ // own kernels is their responsibility, same as for any other tensor.
9721046 auto attached_data = new StandAloneFrameAttachedData ();
9731047 attached_data->producer_stream = current_stream;
9741048 attached_data->storage = std::move (storage);
@@ -1025,19 +1099,8 @@ torch::stable::Tensor BetaCudaDeviceInterface::copy_nvdec_surface(
10251099 " Failed to copy NVDEC surface: " ,
10261100 cudaGetErrorString (err));
10271101
1028- // TODO_API_BREAKDOWN CORRECTNESS P0: We might want to unmap here to clearly
1029- // state that the surface memory can be reused and that there's no leak (and
1030- // rename this into copy_and_unmap_nvdec_surface). However, regardless of
1031- // whether we unmap here or let receive_frame() unmap, I think we have a
1032- // problem: the copy is async, and nothing prevents a PacketDecoder from
1033- // decoding 2 consecutive frames on 2 separate streams. The following can
1034- // happen: with Stream():
1035- // packet_decoder.decode() -> receive_frame() -> copy_nvdec_surface() ->
1036- // cudaMemcpyAsync()
1037- // with Stream():
1038- // packet_decoder.decode() -> receive_frame() -> unmap_previous_frame()
1039- // where unmap_previous_frame() unmaps the surface before the cudaMemcpyAsync
1040- // is able to finish on the other stream.
1102+ // The copy is async, so the next mapping must be ordered after it.
1103+ record_surface_read (current_stream);
10411104
10421105 auto y_plane = static_cast <uint8_t *>(storage.mutable_data_ptr ());
10431106 av_frame->data [0 ] = y_plane;
@@ -1337,6 +1400,17 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
13371400 frame_output.data = convert_frame (/* preAlloc=*/ std::nullopt );
13381401 apply_rotation (frame_output, pre_allocated_output_tensor);
13391402 }
1403+
1404+ if (mode () == Mode::Both && !needs_upload) {
1405+ // The conversion read the mapped surface directly, and did so
1406+ // asynchronously, so the next mapping must be ordered after it.
1407+ // This is only needed in Both() mode because in ColorConverterOnly() mode,
1408+ // the frame is already standalone and doesn't come from the mapped surface.
1409+ // It's also only needed in the non-fallback mode (needs_upload is false)
1410+ // because with the fallback, the GPU frame is a copy of the CPU frame, so
1411+ // the mapped surface isn't read at all.
1412+ record_surface_read (current_stream);
1413+ }
13401414}
13411415
13421416void BetaCudaDeviceInterface::apply_rotation (
0 commit comments