Skip to content

Commit ce3195a

Browse files
committed
Add ROCJPEG decoder
1 parent 68ed43c commit ce3195a

10 files changed

Lines changed: 649 additions & 57 deletions

File tree

.github/workflows/linux_rocm.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ jobs:
5656
package-name: torchcodec
5757
trigger-event: ${{ github.event_name }}
5858
build-platform: "python-build-package"
59-
# CPU-only compile: we build against torch-ROCm, but no ROCm/GPU decode
60-
# sources exist yet (rocJPEG is a follow-up), so ENABLE_CUDA/ENABLE_ROCM
61-
# are intentionally unset and this produces a CPU-only wheel.
62-
build-command: "BUILD_AGAINST_ALL_FFMPEG_FROM_S3=1 python -m build --wheel -vvv --no-isolation"
59+
# ENABLE_ROCM=1 compiles the rocJPEG GPU JPEG decoder into
60+
# libtorchcodec_image.so (the ROCm counterpart of nvJPEG). Video decoding
61+
# stays on CPU. The FFmpeg core libs are still built against all FFmpeg.
62+
build-command: "BUILD_AGAINST_ALL_FFMPEG_FROM_S3=1 ENABLE_ROCM=1 python -m build --wheel -vvv --no-isolation"
6363

6464
install-and-test:
6565
# ROCm runners are not on EC2 and authenticate to AWS (ECR) via GitHub OIDC.

packaging/repair_wheel.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,13 @@ def repair_linux(wheels):
168168
"libnvshmem*",
169169
"libnvfatbin*",
170170
"libnvcuvid*",
171+
# rocJPEG: the GPU JPEG decoder our image lib links on ROCm. Unlike
172+
# nvJPEG (which we bundle), rocJPEG is not shipped by the torch-ROCm
173+
# wheel, and bundling it would drag in torch's ROCm libs under mismatched
174+
# (hashed) sonames. So we treat it as a runtime dependency provided by the
175+
# ROCm install, like FFmpeg. decode_jpeg(device='cuda') therefore needs
176+
# ROCm (with rocJPEG) present at runtime.
177+
"librocjpeg*",
171178
# ROCm/HIP runtime and its system deps: provided by the torch-ROCm wheel
172179
# (torch/lib/) at runtime, exactly like the CUDA libs above. Never bundle
173180
# them — they'd duplicate torch's copies and bloat the wheel.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ location = "source"
5151

5252
[tool.scikit-build.cmake.define]
5353
ENABLE_CUDA = {env = "ENABLE_CUDA", default = ""}
54+
ENABLE_ROCM = {env = "ENABLE_ROCM", default = ""}
5455
TORCHCODEC_DISABLE_COMPILE_WARNING_AS_ERROR = {env = "TORCHCODEC_DISABLE_COMPILE_WARNING_AS_ERROR", default = "OFF"}
5556
TORCHCODEC_DISABLE_HOMEBREW_RPATH = {env = "TORCHCODEC_DISABLE_HOMEBREW_RPATH", default = "OFF"}
5657

@@ -65,6 +66,7 @@ TORCHCODEC_BUILD_AVIF = {env = "TORCHCODEC_BUILD_AVIF", default = "AUTO"}
6566
TORCHCODEC_BUILD_GIF = {env = "TORCHCODEC_BUILD_GIF", default = "AUTO"}
6667
TORCHCODEC_BUILD_HEIC = {env = "TORCHCODEC_BUILD_HEIC", default = "AUTO"}
6768
TORCHCODEC_BUILD_NVJPEG = {env = "TORCHCODEC_BUILD_NVJPEG", default = "AUTO"}
69+
TORCHCODEC_BUILD_ROCJPEG = {env = "TORCHCODEC_BUILD_ROCJPEG", default = "AUTO"}
6870

6971
[project.optional-dependencies]
7072
dev = [

src/torchcodec/_core/CMakeLists.txt

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ endif()
4343
if(NOT DEFINED TORCHCODEC_BUILD_IMAGE)
4444
set(TORCHCODEC_BUILD_IMAGE "ON")
4545
endif()
46-
foreach(_codec JPEG PNG WEBP AVIF GIF HEIC NVJPEG)
46+
foreach(_codec JPEG PNG WEBP AVIF GIF HEIC NVJPEG ROCJPEG)
4747
if(NOT DEFINED TORCHCODEC_BUILD_${_codec})
4848
set(TORCHCODEC_BUILD_${_codec} "AUTO")
4949
endif()
@@ -563,6 +563,41 @@ function(make_torchcodec_image_library)
563563
message(STATUS "Not building torchcodec with nvJPEG support (disabled via TORCHCODEC_BUILD_NVJPEG/TORCHCODEC_BUILD_IMAGE): decode_jpeg(device='cuda') will raise at runtime.")
564564
endif()
565565

566+
# ROCm counterpart of the nvJPEG block above: GPU JPEG decoding via rocJPEG.
567+
# PyTorch exposes AMD GPUs under the "cuda" device string, so decode_jpeg(
568+
# device="cuda") routes to the same decode_jpegs_cuda op, backed by rocJPEG.
569+
resolve_image_codec("${TORCHCODEC_BUILD_ROCJPEG}" want_rocjpeg)
570+
if(ENABLE_ROCM AND want_rocjpeg)
571+
if(DEFINED ENV{ROCM_PATH})
572+
set(ROCM_PATH "$ENV{ROCM_PATH}")
573+
elseif(NOT DEFINED ROCM_PATH)
574+
set(ROCM_PATH "/opt/rocm")
575+
endif()
576+
# So find_package(hip) locates ${ROCM_PATH}/lib/cmake/hip/hip-config.cmake.
577+
list(APPEND CMAKE_PREFIX_PATH "${ROCM_PATH}")
578+
find_package(hip REQUIRED)
579+
find_path(ROCJPEG_INCLUDE_DIR
580+
NAMES rocjpeg/rocjpeg.h
581+
PATHS "${ROCM_PATH}/include")
582+
find_library(ROCJPEG_LIBRARY
583+
NAMES rocjpeg
584+
PATHS "${ROCM_PATH}/lib")
585+
if(NOT ROCJPEG_INCLUDE_DIR OR NOT ROCJPEG_LIBRARY)
586+
message(FATAL_ERROR
587+
"rocJPEG not found (looked under ${ROCM_PATH}), but ROCm GPU JPEG "
588+
"decoding is enabled. Install the rocJPEG runtime and dev headers, "
589+
"or set TORCHCODEC_BUILD_ROCJPEG=0 to build without it "
590+
"(decode_jpeg(device='cuda') will raise at runtime).")
591+
endif()
592+
target_include_directories(${image_library_name} PRIVATE ${ROCJPEG_INCLUDE_DIR})
593+
target_link_libraries(${image_library_name} PRIVATE ${ROCJPEG_LIBRARY} hip::host)
594+
target_compile_definitions(${image_library_name} PRIVATE
595+
TORCHCODEC_ENABLE_ROCJPEG=1 USE_ROCM __HIP_PLATFORM_AMD__)
596+
message(STATUS "Building torchcodec with rocJPEG GPU JPEG decoding support.")
597+
elseif(ENABLE_ROCM)
598+
message(STATUS "Not building torchcodec with rocJPEG support (disabled via TORCHCODEC_BUILD_ROCJPEG/TORCHCODEC_BUILD_IMAGE): decode_jpeg(device='cuda') will raise at runtime.")
599+
endif()
600+
566601
if(want_jpeg)
567602
target_compile_definitions(${image_library_name} PRIVATE TORCHCODEC_ENABLE_JPEG=1)
568603
target_link_libraries(${image_library_name} PRIVATE JPEG::JPEG)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
// All rights reserved.
3+
//
4+
// This source code is licensed under the BSD-style license found in the
5+
// LICENSE file in the root directory of this source tree.
6+
7+
#pragma once
8+
9+
#include <cstring>
10+
11+
#include "Exif.h"
12+
13+
// Helpers shared by the GPU JPEG decoders (nvJPEG on CUDA, rocJPEG on ROCm).
14+
// The CPU JPEG decoder relies on libjpeg for EXIF handling; the GPU decoders
15+
// don't decode metadata, so we scan the bitstream for orientation ourselves.
16+
17+
namespace facebook::torchcodec {
18+
19+
// Scan a JPEG bitstream for the APP1/EXIF segment and return its orientation.
20+
inline exif_private::ExifOrientation fetch_exif_orientation_from_jpeg_bytes(
21+
const unsigned char* jpeg,
22+
size_t size) {
23+
constexpr unsigned char MARKER_PREFIX = 0xFF;
24+
constexpr unsigned char SOI = 0xD8;
25+
constexpr unsigned char SOS = 0xDA; // start of scan: no more metadata markers
26+
constexpr unsigned char EOI = 0xD9;
27+
constexpr unsigned char APP1 = 0xE1;
28+
constexpr size_t exif_header_size = 6; // "Exif\0\0"
29+
30+
if (size < 2 || jpeg[0] != MARKER_PREFIX || jpeg[1] != SOI) {
31+
return exif_private::ExifOrientation::Unspecified;
32+
}
33+
34+
size_t pos = 2;
35+
while (pos + 4 <= size && jpeg[pos] == MARKER_PREFIX) {
36+
unsigned char marker = jpeg[pos + 1];
37+
if (marker == SOS || marker == EOI) {
38+
break;
39+
}
40+
// Segment length is big-endian and includes the 2 length bytes themselves.
41+
size_t segment_length =
42+
(size_t(jpeg[pos + 2]) << 8) | size_t(jpeg[pos + 3]);
43+
if (segment_length < 2 || pos + 2 + segment_length > size) {
44+
break;
45+
}
46+
47+
if (marker == APP1 && segment_length >= 2 + exif_header_size) {
48+
const unsigned char* payload = jpeg + pos + 4;
49+
if (std::memcmp(payload, "Exif\0\0", exif_header_size) == 0) {
50+
return exif_private::fetch_exif_orientation(
51+
payload + exif_header_size, segment_length - 2 - exif_header_size);
52+
}
53+
}
54+
pos += 2 + segment_length;
55+
}
56+
return exif_private::ExifOrientation::Unspecified;
57+
}
58+
59+
} // namespace facebook::torchcodec

src/torchcodec/_core/DecodeJpegCuda.cpp

Lines changed: 7 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111

1212
#include "StableABICompat.h"
1313

14-
#if !TORCHCODEC_ENABLE_NVJPEG
14+
// The "no GPU JPEG support" stub is defined here, but only when neither GPU
15+
// backend is compiled in. When ROCm/rocJPEG is enabled, decode_jpegs_cuda is
16+
// defined in DecodeJpegRocm.cpp instead, so this file compiles to nothing.
17+
#if !TORCHCODEC_ENABLE_NVJPEG && !TORCHCODEC_ENABLE_ROCJPEG
1518

1619
namespace facebook::torchcodec {
1720

@@ -30,12 +33,10 @@ std::vector<torch::stable::Tensor> decode_jpegs_cuda(
3033

3134
} // namespace facebook::torchcodec
3235

33-
#else
34-
35-
#include <cstring>
36+
#elif TORCHCODEC_ENABLE_NVJPEG
3637

3738
#include "CUDACommon.h"
38-
#include "Exif.h"
39+
#include "DecodeJpegCommon.h"
3940
#include "ImageCommon.h"
4041

4142
namespace facebook::torchcodec {
@@ -71,48 +72,6 @@ using namespace exif_private;
7172

7273
namespace {
7374

74-
// Scan a JPEG bitstream for the APP1/EXIF segment and return its orientation.
75-
// On the CPU jpeg decoder, we rely on libjpeg for that. Here, we have to parse
76-
// it ourselves.
77-
ExifOrientation fetch_exif_orientation_from_jpeg_bytes(
78-
const unsigned char* jpeg,
79-
size_t size) {
80-
constexpr unsigned char MARKER_PREFIX = 0xFF;
81-
constexpr unsigned char SOI = 0xD8;
82-
constexpr unsigned char SOS = 0xDA; // start of scan: no more metadata markers
83-
constexpr unsigned char EOI = 0xD9;
84-
constexpr unsigned char APP1 = 0xE1;
85-
constexpr size_t exif_header_size = 6; // "Exif\0\0"
86-
87-
if (size < 2 || jpeg[0] != MARKER_PREFIX || jpeg[1] != SOI) {
88-
return ExifOrientation::Unspecified;
89-
}
90-
91-
size_t pos = 2;
92-
while (pos + 4 <= size && jpeg[pos] == MARKER_PREFIX) {
93-
unsigned char marker = jpeg[pos + 1];
94-
if (marker == SOS || marker == EOI) {
95-
break;
96-
}
97-
// Segment length is big-endian and includes the 2 length bytes themselves.
98-
size_t segment_length =
99-
(size_t(jpeg[pos + 2]) << 8) | size_t(jpeg[pos + 3]);
100-
if (segment_length < 2 || pos + 2 + segment_length > size) {
101-
break;
102-
}
103-
104-
if (marker == APP1 && segment_length >= 2 + exif_header_size) {
105-
const unsigned char* payload = jpeg + pos + 4;
106-
if (std::memcmp(payload, "Exif\0\0", exif_header_size) == 0) {
107-
return fetch_exif_orientation(
108-
payload + exif_header_size, segment_length - 2 - exif_header_size);
109-
}
110-
}
111-
pos += 2 + segment_length;
112-
}
113-
return ExifOrientation::Unspecified;
114-
}
115-
11675
// We cache decoder objects for the same reason we cache NVDEC decoders: they're
11776
// expensive to create and destroy. To determine the ideal cache size, I ran
11877
// benchmarks on a A100 where each thread calls its own decode_jpeg():
@@ -520,4 +479,4 @@ std::vector<torch::stable::Tensor> CUDAJpegDecoder::decode_images(
520479

521480
} // namespace facebook::torchcodec
522481

523-
#endif // !TORCHCODEC_ENABLE_NVJPEG
482+
#endif // GPU JPEG backend selection

0 commit comments

Comments
 (0)