@@ -1019,32 +1019,6 @@ void BetaCudaDeviceInterface::make_frame_standalone(UniqueAVFrame& av_frame) {
10191019 storage = copy_nvdec_surface (av_frame, current_stream);
10201020 }
10211021
1022- // TODO_API_BREAKDOWN CORRECTNESS P0: `storage` comes from the PyTorch
1023- // caching allocator on `current_stream`, but a ColorConverter reads it from
1024- // whatever stream it runs on. The allocator only tracks the allocating
1025- // stream: once the frame is dropped, the block returns to `current_stream`'s
1026- // pool with no synchronisation, and the next frame's copy - same size, same
1027- // stream - lands right in it while the converter is still reading. Frames
1028- // are then silently corrupted whenever the converter lags behind the
1029- // decoder, which is the normal state of a two-stream pipeline. Measured on a
1030- // 4K clip with the converter backlogged: 117 of 119 frames wrong, and clean
1031- // again as soon as the frames are kept alive.
1032- // The usual remedy is Tensor::record_stream() on the consumer side, but
1033- // neither the stable ABI nor the AOTI shim exposes it, and StableIValue has
1034- // no Stream conversion, so it isn't reachable through the dispatcher either.
1035- // We don't actually need it though: the allocator recycles the block because
1036- // *we* drop our reference too early, so it's enough to hold on to the
1037- // storage until the consumer is done. Have the ColorConverter record an
1038- // event on its own stream into the attached data, and make
1039- // standalone_frame_free_callback() hand (storage, event) to a per-device
1040- // pending-release list instead of dropping the tensor. Drain that list
1041- // opportunistically with cudaEventQuery. No host stall, and a consumer that
1042- // permanently lags shows up as a growing list, i.e. as backpressure rather
1043- // than as silent corruption.
1044- // Frames that were never converted have no event and can be released
1045- // straight away. Raw planes handed to the user via materialize() keep the
1046- // storage alive on their own, and once the user drops those, ordering their
1047- // own kernels is their responsibility, same as for any other tensor.
10481022 auto attached_data = new StandAloneFrameAttachedData ();
10491023 attached_data->producer_stream = current_stream;
10501024 attached_data->storage = std::move (storage);
@@ -1059,6 +1033,62 @@ void BetaCudaDeviceInterface::make_frame_standalone(UniqueAVFrame& av_frame) {
10591033 " Failed to attach standalone frame data" );
10601034}
10611035
1036+ std::optional<torch::stable::Tensor> BetaCudaDeviceInterface::get_frame_storage (
1037+ const AVFrame& av_frame) const {
1038+ STD_TORCH_CHECK (
1039+ // Only decoder-only should reach here, and this should only be called on
1040+ // frames that went through make_frame_standalone(), which sets
1041+ // opaque_ref.
1042+ mode () == Mode::DecoderOnly && av_frame.opaque_ref != nullptr ,
1043+ " Unexpected call to get_frame_storage(), please report a bug " );
1044+
1045+ // Note [Standalone Frame Storage and the need for record_stream]
1046+ //
1047+ // A PacketDecoder and a ColorConverter may run on different CUDA streams.
1048+ // Consider the following:
1049+ //
1050+ // ```
1051+ // with decoder_stream:
1052+ // frame = decoder.receive_frame()
1053+ // with color_converter_stream:
1054+ // color_converter.convert(frame)
1055+ //
1056+ // del frame
1057+ //
1058+ // with decoder_stream:
1059+ // frame = decoder.receive_frame()
1060+ // ```
1061+ //
1062+ // The call to convert(frame) is non-blocking and just enqueues the
1063+ // color-conversion kernel. The CPU moves on immediately to `del frame` while
1064+ // the kernel is still running (it may also not even have started depending on
1065+ // how color_converter_stream is congested).
1066+ //
1067+ // When the frame is deleted, the torch CUDA allocator reclaims its memory and
1068+ // it becomes available for reuse for any subsequent allocation on the
1069+ // decoder_stream. If the next decoder.receive_frame() happens before the
1070+ // color-conversion kernel has finished (specifically: the new storage
1071+ // allocation for that next frame in make_frame_standalone()), the memory is
1072+ // reused, overwritten, and the color-conversion kernel reads garbage (i.e.
1073+ // the next frame's samples!).
1074+ //
1075+ // We're hitting exactly what
1076+ // https://zdevito.github.io/2022/08/04/cuda-caching-allocator.html describes
1077+ // in the 'Streams and freeing memory' section, and the solution is to call
1078+ // record_stream() on the frame's storage within color_conversion_stream just
1079+ // after the kernel is enqueued: this tells the allocator that it must wait
1080+ // until this point (on the device side) before reclaiming the memory.
1081+ //
1082+ // We call record_stream(color_conversion_stream) on the frame storage in the
1083+ // ColorConverter, on behalf of the user. But we still must expose the storage
1084+ // for those users who would like to consume the frame with their own
1085+ // consumer, i.e. not using the ColorConverter: they need to call
1086+ // frame.storage.record_stream(color_conversion_stream) themselves.
1087+ return reinterpret_cast <StandAloneFrameAttachedData*>(
1088+ av_frame.opaque_ref ->data )
1089+ ->storage ;
1090+ }
1091+
10621092torch::stable::Tensor BetaCudaDeviceInterface::copy_nvdec_surface (
10631093 UniqueAVFrame& av_frame,
10641094 cudaStream_t current_stream) {
0 commit comments