@@ -266,6 +266,8 @@ std::optional<cudaVideoSurfaceFormat> get_nvdec_surface_format(
266266
267267// Callback for freeing CUDA memory associated with AVFrame see where it's used
268268// for more details.
269+ // TODO_API_BREAKDOWN P2: Should we align this with the other free callback
270+ // below? Why did we use cudaMalloc? Can we just allocate with torch??
269271void cuda_buffer_free_callback (void * opaque, [[maybe_unused]] uint8_t * data) {
270272 cudaFree (opaque);
271273}
@@ -276,6 +278,36 @@ void standalone_frame_free_callback(
276278 delete reinterpret_cast <StandAloneFrameAttachedData*>(data);
277279}
278280
281+ class CudaContextGuard {
282+ // There's one CUDA context per process per device. But new threads aren't
283+ // bound to a context. The binding often happens automatically when calling
284+ // CUDA APIs (like cudaFree), but some APIs like the NVCUVID ones that we use
285+ // here aren't automatically binding.
286+ // So for a thread to be able to use NVCUVID APIs, it must have a context
287+ // bound to it, and we have to enforce that binding manually.
288+ // That's what this guard does: it calls cudaFree(nullptr), which is a common
289+ // near-free way to force the CUDA runtime to bind the context for the current
290+ // thread. And this call must happen within a device guard to make sure we're
291+ // binding the context of the device this interface is using.
292+ // We must call this guard in every public method of the interface that uses
293+ // NVCUVID APIs, because these methods can, in theory, be called from any
294+ // thread.
295+ // Note that none of this was an issue before when our only entry-point was
296+ // the SingleStreamDecoder: all the entry-points were called from the same
297+ // thread. Now that we have split the APIs in different blocks (PacketDecoder,
298+ // ColorConverter), each of these blocks can be on different threads - and
299+ // importantly, they can be created in the main thread (where the context is
300+ // bound by our call to initialize_cuda_context_with_pytorch()), but then used
301+ // in a different thread that doesn't have the context.
302+ public:
303+ explicit CudaContextGuard (int device_index) : device_guard_(device_index) {
304+ cudaFree (nullptr );
305+ }
306+
307+ private:
308+ StableDeviceGuard device_guard_;
309+ };
310+
279311} // namespace
280312
281313BetaCudaDeviceInterface::BetaCudaDeviceInterface (const StableDevice& device)
@@ -284,6 +316,9 @@ BetaCudaDeviceInterface::BetaCudaDeviceInterface(const StableDevice& device)
284316 STD_TORCH_CHECK (
285317 device_.type () == kStableCUDA , " Unsupported device: must be CUDA" );
286318
319+ // Note: now that we have the CudaContextGuard, we might not need to do that
320+ // anymore. The comment says we need pytorch to create the context - maybe
321+ // that's true, but that's a very old comment now.
287322 initialize_cuda_context_with_pytorch (device_);
288323
289324 nvcuvid_available_ = load_nvcuvid_library ();
@@ -295,11 +330,12 @@ void BetaCudaDeviceInterface::initialize_video(
295330 const VideoStreamOptions& video_stream_options,
296331 const std::vector<std::unique_ptr<Transform>>& transforms,
297332 const std::optional<FrameDims>& resized_output_dims) {
298- // TODO_API_BREAKDOWN ewwwww
333+ // TODO_API_BREAKDOWN P0
299334 if (!av_stream) {
300335 return ;
301336 }
302337 STD_TORCH_CHECK (av_stream != nullptr , " AVStream cannot be null" );
338+ CudaContextGuard context_guard (device_.index ());
303339 rotation_ = rotation_from_degrees (get_rotation_from_stream (av_stream));
304340 output_dtype_ = video_stream_options.output_dtype ;
305341
@@ -403,6 +439,7 @@ void BetaCudaDeviceInterface::send_seqhdr_packet() {
403439}
404440
405441BetaCudaDeviceInterface::~BetaCudaDeviceInterface () {
442+ CudaContextGuard context_guard (device_.index ());
406443 if (decoder_) {
407444 // DALI doesn't seem to do any particular cleanup of the decoder before
408445 // sending it to the cache, so we probably don't need to do anything either.
@@ -554,6 +591,7 @@ int BetaCudaDeviceInterface::stream_property_change(
554591// Moral equivalent of avcodec_send_packet(). Here, we pass the AVPacket down to
555592// the NVCUVID parser.
556593int BetaCudaDeviceInterface::send_packet (ReferenceAVPacket& packet) {
594+ CudaContextGuard context_guard (device_.index ());
557595 if (cpu_fallback_) {
558596 return cpu_fallback_->send_packet (packet);
559597 }
@@ -581,6 +619,7 @@ int BetaCudaDeviceInterface::send_packet(ReferenceAVPacket& packet) {
581619}
582620
583621int BetaCudaDeviceInterface::send_eof_packet () {
622+ CudaContextGuard context_guard (device_.index ());
584623 if (cpu_fallback_) {
585624 return cpu_fallback_->send_eof_packet ();
586625 }
@@ -656,6 +695,7 @@ int BetaCudaDeviceInterface::frame_ready_in_display_order(
656695
657696// Moral equivalent of avcodec_receive_frame().
658697int BetaCudaDeviceInterface::receive_frame (UniqueAVFrame& av_frame) {
698+ CudaContextGuard context_guard (device_.index ());
659699 if (cpu_fallback_) {
660700 return cpu_fallback_->receive_frame (av_frame);
661701 }
@@ -698,8 +738,8 @@ int BetaCudaDeviceInterface::receive_frame(UniqueAVFrame& av_frame) {
698738 // color-converted (with a copy), or that's a frame that was discarded in
699739 // SingleStreamDecoder. Either way, the underlying output surface can be
700740 // safely re-used.
701- // TODO_API_BREAKDOWN: We should update this comment slightly to now account
702- // for the frame copy we do in make_frame_standalone()
741+ // TODO_API_BREAKDOWN P1 : We should update this comment slightly to now
742+ // account for the frame copy we do in make_frame_standalone()
703743 unmap_previous_frame ();
704744 CUresult result = cuvidMapVideoFrame (
705745 *decoder_.get (),
@@ -807,7 +847,7 @@ UniqueAVFrame BetaCudaDeviceInterface::convert_cuda_frame_to_av_frame(
807847 reinterpret_cast <uint8_t *>(frame_ptr + (pitch * even_height));
808848 av_frame->data [2 ] = nullptr ;
809849 av_frame->data [3 ] = nullptr ;
810- // TODO_API_BREAKDOWN_CUDA: Check range before cast?
850+ // TODO_API_BREAKDOWN_CUDA P2 : Check range before cast?
811851 av_frame->linesize [0 ] = static_cast <int >(pitch);
812852 av_frame->linesize [1 ] = static_cast <int >(pitch);
813853 av_frame->linesize [2 ] = 0 ;
@@ -816,10 +856,8 @@ UniqueAVFrame BetaCudaDeviceInterface::convert_cuda_frame_to_av_frame(
816856 return av_frame;
817857}
818858
819- // TODO_API_BREAKDOWN_CUDA: Does this even nede to be a method? Maybe it can be
820- // a function that just lives in the PacketDecoder so we don't need to expose
821- // another API to the DeviceInterface?
822859void BetaCudaDeviceInterface::make_frame_standalone (UniqueAVFrame& av_frame) {
860+ CudaContextGuard context_guard (device_.index ());
823861 if (!(av_frame->format == AV_PIX_FMT_P016LE ||
824862 av_frame->format == AV_PIX_FMT_NV12 )) {
825863 // The CPU frames are already standalone, so we don't need to do anything.
@@ -832,7 +870,7 @@ void BetaCudaDeviceInterface::make_frame_standalone(UniqueAVFrame& av_frame) {
832870 // = num_pixels * 3 / 2
833871 //
834872 // To make it correct, we should use num_pixels = pitch * height, not
835- // num_pixels = pitch * height. The pitch value also accounts for the data
873+ // num_pixels = width * height. The pitch value also accounts for the data
836874 // size (uint8 vs uint16) so this is also correct for P016.
837875 int64_t even_height =
838876 static_cast <int64_t >(round_up_to_even (av_frame->height ));
@@ -842,7 +880,7 @@ void BetaCudaDeviceInterface::make_frame_standalone(UniqueAVFrame& av_frame) {
842880 auto storage =
843881 torch::stable::empty ({num_bytes}, kStableUInt8 , std::nullopt , device_);
844882
845- // TODO_API_BREAKDOWN_CUDA: I suspect we don't need to wait on the nvdec
883+ // TODO_API_BREAKDOWN_CUDA P1 : I suspect we don't need to wait on the nvdec
846884 // stream here, because we can only arrive here from a path where the frame
847885 // has already been mapped so its data is available - worth double checking.
848886 cudaStream_t current_stream = get_current_cuda_stream (device_.index ());
@@ -857,7 +895,7 @@ void BetaCudaDeviceInterface::make_frame_standalone(UniqueAVFrame& av_frame) {
857895 " Failed to copy NVDEC surface: " ,
858896 cudaGetErrorString (err));
859897
860- // TODO_API_BREAKDOWN_CUDA: Should we unmap here? Or let the next
898+ // TODO_API_BREAKDOWN_CUDA P2 : Should we unmap here? Or let the next
861899 // receive_frame() call do it?
862900 // unmap_previous_frame();
863901
@@ -867,7 +905,7 @@ void BetaCudaDeviceInterface::make_frame_standalone(UniqueAVFrame& av_frame) {
867905
868906 auto attached_data = new StandAloneFrameAttachedData ();
869907 attached_data->producer_stream = current_stream;
870- // TODO_API_BREAKDOWN_CUDA: We don't *really* need to std::move it I guess?
908+ // TODO_API_BREAKDOWN_CUDA P2 : We don't *really* need to std::move it I guess?
871909 attached_data->storage = std::move (storage);
872910 av_frame->opaque_ref = av_buffer_create (
873911 reinterpret_cast <uint8_t *>(attached_data),
@@ -878,6 +916,7 @@ void BetaCudaDeviceInterface::make_frame_standalone(UniqueAVFrame& av_frame) {
878916}
879917
880918void BetaCudaDeviceInterface::flush () {
919+ CudaContextGuard context_guard (device_.index ());
881920 if (cpu_fallback_) {
882921 cpu_fallback_->flush ();
883922 return ;
@@ -1044,9 +1083,10 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
10441083 const AVFrame& av_frame,
10451084 FrameOutput& frame_output,
10461085 std::optional<torch::stable::Tensor> pre_allocated_output_tensor) {
1047- // TODO_API_BREAKDOWN_CUDA is that accurate and safe? Can there be a CPU NV12
1048- // frame in our code? Should we create a helper used in the make_standalone
1049- // function too?
1086+ CudaContextGuard context_guard (device_.index ());
1087+ // TODO_API_BREAKDOWN_CUDA P0 is that accurate and safe? Can there be a CPU
1088+ // NV12 frame in our code? Should we create a helper used in the
1089+ // make_standalone function too?
10501090 bool cpu_fallback = av_frame.format != AV_PIX_FMT_NV12 &&
10511091 av_frame.format != AV_PIX_FMT_P016LE ;
10521092
@@ -1062,7 +1102,7 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
10621102 av_pix_fmt_desc_get (static_cast <AVPixelFormat>(av_frame.format ));
10631103 bool is444 = desc && desc->log2_chroma_w == 0 && desc->log2_chroma_h == 0 ;
10641104 if (is444) {
1065- // TODO_API_BREAKDOWN we need to handle this
1105+ // TODO_API_BREAKDOWN P1: we need to handle this
10661106 FrameOutput cpu_frame_output;
10671107 cpu_fallback_->convert_av_frame_to_frame_output (
10681108 av_frame, cpu_frame_output);
@@ -1098,7 +1138,7 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
10981138 gpu_frame.format == AV_PIX_FMT_P016LE ,
10991139 " Expected NV12 or P016LE format frame" );
11001140
1101- // TODO_API_BREAKDOWN: Cleanup how we get the attached data? Make it more
1141+ // TODO_API_BREAKDOWN P1 : Cleanup how we get the attached data? Make it more
11021142 // robust? Should we couple it to a flag on the interface saying "I'm
11031143 // color-conversion only, I absolutely expect frames to be standalone"?
11041144 cudaStream_t producer_stream;
@@ -1111,7 +1151,7 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
11111151 producer_stream = get_current_cuda_stream (device_.index ());
11121152 }
11131153
1114- // TODO_API_BREAKDOWN: we don't suppor output_dtype so some of that is not
1154+ // TODO_API_BREAKDOWN P1 : we don't suppor output_dtype so some of that is not
11151155 // execrcized.
11161156 auto convert_frame = [&](std::optional<torch::stable::Tensor> pre_alloc)
11171157 -> torch::stable::Tensor {
0 commit comments