Skip to content

Commit f42f9e0

Browse files
authored
Fix 4:4:4 video decoding on CUDA interface (#1415)
1 parent d1a1197 commit f42f9e0

2 files changed

Lines changed: 47 additions & 0 deletions

File tree

src/torchcodec/_core/BetaCudaDeviceInterface.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,33 @@ void BetaCudaDeviceInterface::convertAVFrameToFrameOutput(
903903
UniqueAVFrame& avFrame,
904904
FrameOutput& frameOutput,
905905
std::optional<torch::stable::Tensor> preAllocatedOutputTensor) {
906+
if (cpuFallback_) {
907+
// When the CPU fallabck happens, we'll try to run the color-conversion on
908+
// GPU by sending those CPU frames to the GPU as NV12 (See
909+
// transferCpuFrameToGpuNV12() below). However, it's not always possible:
910+
// NV12 would downsample 4:4:4 frames and lose chroma resolution, resulting
911+
// in poorly decoded frames. So for those, we still do the color conversion
912+
// on the CPU and then send the full RGB frame to the GPU.
913+
const AVPixFmtDescriptor* desc =
914+
av_pix_fmt_desc_get(static_cast<AVPixelFormat>(avFrame->format));
915+
if (desc && desc->log2_chroma_w == 0 && desc->log2_chroma_h == 0) {
916+
// 4:4:4: converting through NV12 (4:2:0) would lose chroma resolution.
917+
FrameOutput cpuFrameOutput;
918+
cpuFallback_->convertAVFrameToFrameOutput(avFrame, cpuFrameOutput);
919+
if (preAllocatedOutputTensor.has_value()) {
920+
torch::stable::copy_(
921+
preAllocatedOutputTensor.value(), cpuFrameOutput.data);
922+
frameOutput.data = preAllocatedOutputTensor.value();
923+
} else {
924+
frameOutput.data = torch::stable::to(cpuFrameOutput.data, device_);
925+
}
926+
if (rotation_ != Rotation::NONE) {
927+
applyRotation(frameOutput, preAllocatedOutputTensor);
928+
}
929+
return;
930+
}
931+
}
932+
906933
// Capture original dimensions before transferCpuFrameToGpuNV12 may
907934
// round them up to even for NV12.
908935
FrameDims originalDims(avFrame->height, avFrame->width);

test/test_decoders.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
)
2424
from torchcodec.decoders._decoder_utils import _get_cuda_backend
2525
from torchcodec.decoders._wav_decoder import WavDecoder
26+
from torchcodec.encoders import VideoEncoder
2627
from torchcodec.transforms import CenterCrop, RandomCrop, Resize
2728

2829
from .utils import (
@@ -2032,6 +2033,25 @@ def test_nvdec_cuda_interface_cpu_fallback(self):
20322033

20332034
assert psnr(ref_frame.data, nvdec_frame.data) > 25
20342035

2036+
@needs_cuda
2037+
def test_nvdec_cpu_fallback_yuv444(self, tmp_path):
2038+
# Non-regression test for https://github.com/meta-pytorch/torchcodec/issues/1414
2039+
num_frames = 5
2040+
frames = torch.randint(0, 256, size=(num_frames, 3, 64, 64), dtype=torch.uint8)
2041+
path = str(tmp_path / "yuv444.mp4")
2042+
VideoEncoder(frames=frames, frame_rate=30).to_file(
2043+
path, pixel_format="yuv444p", crf=0
2044+
)
2045+
2046+
cpu_decoder = VideoDecoder(path, device="cpu")
2047+
cuda_decoder = VideoDecoder(path, device="cuda")
2048+
assert cuda_decoder.cpu_fallback
2049+
2050+
cpu_frames = cpu_decoder.get_frames_in_range(start=0, stop=num_frames).data
2051+
cuda_frames = cuda_decoder.get_frames_in_range(start=0, stop=num_frames).data
2052+
2053+
torch.testing.assert_close(cpu_frames, cuda_frames.cpu(), rtol=0, atol=0)
2054+
20352055
@needs_cuda
20362056
def test_nvdec_cuda_interface_error(self):
20372057
with pytest.raises(RuntimeError, match="torch_parse_device_string"):

0 commit comments

Comments
 (0)