Skip to content

Commit eea2762

Browse files
committed
Port pulsar to ROCm/HIP
Re-enables the pulsar subrenderer on ROCm-built PyTorch. Pulsar was excluded from the ROCm build in the parent commit (PR #2039) because its 74 source files use a mix of CUDA intrinsics with no direct HIP equivalent. This commit makes it build and run on AMD MI250X (gfx90a, warpSize=64, HIP 7.2) using only HIP runtime APIs and standard math — no `amdgcn` inline assembly. Most of the lift is build-system reversal. The pulsar source filter in `setup.py`, the `#ifndef USE_ROCM` wraps around the pulsar `#include`s and pybind block in `pytorch3d/csrc/ext.cpp`, and the `hasattr(_C, "PulsarRenderer")` guards in the three `pytorch3d/renderer/.../__init__.py` files all go away. The only C++ surgery is in `pytorch3d/csrc/pulsar/gpu/commands.h`: The CUDA `_rn`-suffixed FP rounding intrinsics (`__fadd_rn`, `__fdiv_rn`, `__fsqrt_rn`, `__fmaf_rn`, `__frcp_rn`) and `__saturatef` have no HIP equivalents. AMD's GPU ISA has no instruction-level rounding-mode override — the rounding mode lives in the wavefront `MODE` register, defaulting to round-to-nearest-even. So under `USE_ROCM` these macros expand to plain operators / `sqrtf` / `fmaf` / `1.0f/x` / `fmaxf(0,fminf(1,x))`, which are rounding-mode-equivalent on HIP. The HIP/clang compiler may fuse `a+b*c` into a single-rounding FMA where CUDA's `_rn` would have prevented it; the upstream pulsar authors already accept compiler-discretion FMA fusion for `FMUL`/`FSUB` (the `_rn` variants are commented out in the source), and pulsar's tests pass under the same trade-off on HIP. If FMA-fusion drift ever surfaces as a numerical issue, add `-ffp-contract=off` to pulsar's HIPCC flags. `__powf` is replaced with `powf` for clarity on the ROCm arm. `atomicAdd_block` has no HIP function-name equivalent, but the *semantic* equivalent is `__hip_atomic_fetch_add(ptr, val, __ATOMIC_RELAXED, __HIP_MEMORY_SCOPE_WORKGROUP)`. Plain HIP `atomicAdd` is device-scope (`__HIP_MEMORY_SCOPE_AGENT`, see `amd_hip_atomic.h:218`), which is strictly stronger than block-scope — correct but forces L2-coherent atomics on what are block-local counters at `renderer.render.device.h:186,297` (sphere-loading counter and per-pixel-done counter). The workgroup-scoped builtin gets us back to the cheaper LDS atomic path that CUDA's `atomicAdd_block` uses. Verified absent-from-HIP and explicitly replaced: `__fdiv_rn`, `__fadd_rn`, `__fsqrt_rn`, `__fmaf_rn`, `__frcp_rn`, `__saturatef`, `atomicAdd_block` (semantic replacement via `__hip_atomic_fetch_add` + `__HIP_MEMORY_SCOPE_WORKGROUP`). Verified present-in-HIP and left alone: `hsqrt` (`amd_hip_fp16.h`), `__int2float_rn` (`amd_device_functions.h:536`, defined as `(float)x`), `__hadd`/`__hsub2`/`__hmul2` (`amd_hip_fp16.h`), `__clz`/`__popc`/`__popcll`/`__mul24` (`amd_device_functions.h`), native `atomicMin(float*,float)`/`atomicMax(float*,float)` (`amd_hip_atomic.h`), and the existing `__HIP_PLATFORM_AMD__` `__ballot`+`__popcll` warpSize=64 workaround at `include/renderer.render.device.h:289-295`. The unreferenced 32-lane WARP_CUMSUM/WARP_MAX/WARP_SUM helpers at `gpu/commands.h:66-123` remain gated out on ROCm — the TODO comment about ROCM-6.2 was technically right that HIP supports `__shfl_*_sync` now, but since nothing in pulsar actually calls these helpers, there's no live warpSize=32 assumption to fix. Test results on AMD Instinct MI250X (gfx90a, HIP 7.2): tests/pulsar/test_forward.py 5 passed tests/pulsar/test_channels.py 1 passed tests/pulsar/test_depth.py 1 passed tests/pulsar/test_hands.py 1 passed tests/pulsar/test_ortho.py 1 passed tests/pulsar/test_small_spheres.py 1 passed tests/test_render_points.py::test_simple_sphere_pulsar 1 passed tests/test_render_points.py::test_unified_inputs_pulsar 1 passed tests/test_camera_conversions.py::test_pulsar_conversion 1 passed Pulsar tests need `FB_TEST=1` because `tests/pulsar/test_forward.py::test_principal_point` writes a 1-channel debug PNG via `imageio.imsave`, and current `imageio`'s pillow backend refuses 1-channel writes (`ValueError: Can't write images with one color channel.`). The test gates the imsave block on `not os.environ.get("FB_TEST", False)`; the actual GPU render and `np.allclose` assertion that follow have nothing to do with imageio. This is an imageio/pillow versioning issue, not the HIP port. Parent PR's 123 core CUDA-kernel tests still pass on the same host — no regression. .gitignore now also excludes `tests/pulsar/test_out/` (PNG debug dumps written when FB_TEST is unset). Co-Authored-By: Claude Opus 4.7 (1M context)
1 parent 776489a commit eea2762

7 files changed

Lines changed: 39 additions & 51 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ pytorch3d/csrc/**/*_hip.cpp
1414
pytorch3d/csrc/**/*_hip.h
1515
pytorch3d/csrc/**/*_hip.cuh
1616

17+
# Debug PNG dumps written by pulsar tests when FB_TEST is unset.
18+
tests/pulsar/test_out/
19+
1720

1821
# Docusaurus site
1922
website/yarn.lock

pytorch3d/csrc/ext.cpp

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,11 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
// Pulsar is not currently portable to ROCm/HIP and is excluded from the
10-
// build there. Tracked as a separate follow-up port.
11-
#ifndef USE_ROCM
129
// clang-format off
1310
#include "./pulsar/global.h" // Include before <torch/extension.h>.
1411
// clang-format on
1512
#include "./pulsar/pytorch/renderer.h"
1613
#include "./pulsar/pytorch/tensor_util.h"
17-
#endif // !USE_ROCM
1814
#include "ball_query/ball_query.h"
1915
#include "blending/sigmoid_alpha_blend.h"
2016
#include "compositing/alpha_composite.h"
@@ -102,8 +98,6 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
10298
m.def("marching_cubes", &MarchingCubes);
10399

104100
// Pulsar.
105-
// Pulsar not enabled on AMD.
106-
#ifndef USE_ROCM
107101
#ifdef PULSAR_LOGGING_ENABLED
108102
c10::ShowLogInfoToStderr();
109103
#endif
@@ -189,5 +183,4 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
189183
m.attr("MAX_UINT") = py::int_(MAX_UINT);
190184
m.attr("MAX_USHORT") = py::int_(MAX_USHORT);
191185
m.attr("PULSAR_MAX_GRAD_SPHERES") = py::int_(MAX_GRAD_SPHERES);
192-
#endif // !USE_ROCM
193186
}

pytorch3d/csrc/pulsar/gpu/commands.h

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,29 +125,45 @@ INLINE DEVICE float3 WARP_SUM_FLOAT3(
125125
// Floating point.
126126
// #define FMUL(a, b) __fmul_rn((a), (b))
127127
#define FMUL(a, b) ((a) * (b))
128-
#define FDIV(a, b) __fdiv_rn((a), (b))
129128
// #define FSUB(a, b) __fsub_rn((a), (b))
130129
#define FSUB(a, b) ((a) - (b))
130+
// HIP has no per-instruction rounding-mode override (round-to-nearest-even is
131+
// the default at the hardware level), so the CUDA *_rn intrinsics have no
132+
// runtime equivalent and we use plain operators. The HIP/clang compiler may
133+
// fuse a+b*c into a single-rounding FMA where CUDA's _rn would have prevented
134+
// it; if that becomes a numerical issue, add `-ffp-contract=off` to the pulsar
135+
// nvcc_args in setup.py.
136+
#if defined(USE_ROCM)
137+
#define FDIV(a, b) ((a) / (b))
138+
#define FADD(a, b) ((a) + (b))
139+
#define FSQRT(a) sqrtf(a)
140+
#define FPOW(a, b) powf((a), (b))
141+
#define FSATURATE(x) fmaxf(0.0f, fminf(1.0f, (x)))
142+
#define FMA(x, y, z) fmaf((x), (y), (z))
143+
#define FRCP(x) (1.0f / (x))
144+
#else
145+
#define FDIV(a, b) __fdiv_rn((a), (b))
131146
#define FADD(a, b) __fadd_rn((a), (b))
132147
#define FSQRT(a) __fsqrt_rn(a)
148+
#define FPOW(a, b) __powf((a), (b))
149+
#define FSATURATE(x) __saturatef(x)
150+
/** Calculates x*y+z. */
151+
#define FMA(x, y, z) __fmaf_rn((x), (y), (z))
152+
#define FRCP(x) __frcp_rn(x)
153+
#endif
133154
#define FEXP(a) fasterexp(a)
134155
#define FLN(a) fasterlog(a)
135-
#define FPOW(a, b) __powf((a), (b))
136156
#define FMAX(a, b) fmax((a), (b))
137157
#define FMIN(a, b) fmin((a), (b))
138158
#define FCEIL(a) ceilf(a)
139159
#define FFLOOR(a) floorf(a)
140160
#define FROUND(x) nearbyintf(x)
141-
#define FSATURATE(x) __saturatef(x)
142161
#define FABS(a) abs(a)
143162
#define IASF(a, loc) (loc) = __int_as_float(a)
144163
#define FASI(a, loc) (loc) = __float_as_int(a)
145164
#define FABSLEQAS(a, b, c) \
146165
((a) <= (b) ? FSUB((b), (a)) <= (c) : FSUB((a), (b)) < (c))
147-
/** Calculates x*y+z. */
148-
#define FMA(x, y, z) __fmaf_rn((x), (y), (z))
149166
#define I2F(a) __int2float_rn(a)
150-
#define FRCP(x) __frcp_rn(x)
151167
#if !defined(USE_ROCM)
152168
__device__ static float atomicMax(float* address, float val) {
153169
int* address_as_i = (int*)address;
@@ -201,8 +217,17 @@ __device__ static float atomicMin(float* address, float val) {
201217
ATOMICADD(&((PTR)->x), VAL.x); \
202218
ATOMICADD(&((PTR)->y), VAL.y); \
203219
ATOMICADD(&((PTR)->z), VAL.z);
204-
#if (CUDART_VERSION >= 10000) && (__CUDA_ARCH__ >= 600)
220+
#if !defined(USE_ROCM) && \
221+
(CUDART_VERSION >= 10000) && (__CUDA_ARCH__ >= 600)
205222
#define ATOMICADD_B(PTR, VAL) atomicAdd_block((PTR), (VAL))
223+
#elif defined(USE_ROCM)
224+
// HIP has no atomicAdd_block, but the semantic equivalent is a relaxed
225+
// fetch_add scoped to the workgroup (HIP's name for a CUDA thread block).
226+
// This avoids device-wide L2-coherent atomics for what are block-local
227+
// counters in pulsar's inner sphere-loading loop.
228+
#define ATOMICADD_B(PTR, VAL) \
229+
__hip_atomic_fetch_add((PTR), (VAL), __ATOMIC_RELAXED, \
230+
__HIP_MEMORY_SCOPE_WORKGROUP)
206231
#else
207232
#define ATOMICADD_B(PTR, VAL) ATOMICADD(PTR, VAL)
208233
#endif

pytorch3d/renderer/__init__.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,9 @@
7575
PointsRasterizationSettings,
7676
PointsRasterizer,
7777
PointsRenderer,
78+
PulsarPointsRenderer,
7879
rasterize_points,
7980
)
80-
81-
# PulsarPointsRenderer is only exposed when the native pulsar renderer was
82-
# built into pytorch3d._C (CUDA builds). On ROCm builds, pulsar is excluded
83-
# and the symbol simply will not exist on this module.
84-
try:
85-
from .points import PulsarPointsRenderer # noqa: F401
86-
except ImportError:
87-
pass
8881
from .splatter_blend import SplatterBlender
8982
from .utils import (
9083
convert_to_tensors_and_broadcast,

pytorch3d/renderer/points/__init__.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,11 @@
88

99
import torch
1010

11-
from pytorch3d import _C
12-
1311
from .compositor import AlphaCompositor, NormWeightedCompositor
12+
from .pulsar.unified import PulsarPointsRenderer
1413
from .rasterize_points import rasterize_points
1514
from .rasterizer import PointsRasterizationSettings, PointsRasterizer
1615
from .renderer import PointsRenderer
1716

1817

19-
# PulsarPointsRenderer wraps the native pulsar renderer, which is not yet
20-
# available on ROCm builds. Only expose the wrapper when the native class is
21-
# present so that ``import pytorch3d.renderer`` succeeds on AMD GPUs.
22-
if hasattr(_C, "PulsarRenderer"):
23-
from .pulsar.unified import PulsarPointsRenderer # noqa: F401
24-
25-
2618
__all__ = [k for k in globals().keys() if not k.startswith("_")]

pytorch3d/renderer/points/pulsar/__init__.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,4 @@
66

77
# pyre-unsafe
88

9-
from pytorch3d import _C
10-
11-
12-
# The pulsar native renderer is not yet ported to ROCm/HIP; the C++ extension
13-
# omits PulsarRenderer when built for AMD. Importing the Python wrapper would
14-
# crash at class-definition time on those builds (it references _C.MAX_UINT /
15-
# _C.PulsarRenderer at module load). Gate the wrapper symbol on availability
16-
# of the native class so the rest of pytorch3d.renderer remains importable.
17-
if hasattr(_C, "PulsarRenderer"):
18-
from .renderer import Renderer # noqa: F401
9+
from .renderer import Renderer # noqa: F401

setup.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,7 @@ def get_extensions():
5959
include_dirs = [extensions_dir]
6060

6161
# ROCm/HIP support. When PyTorch is built with HIP, the cpp_extension
62-
# BuildExtension auto-hipifies .cu sources and swaps nvcc -> hipcc. Pulsar
63-
# is not currently portable to ROCm and is excluded from the build; see
64-
# the comment in pytorch3d/csrc/ext.cpp ("Pulsar not enabled on AMD").
62+
# BuildExtension auto-hipifies .cu sources and swaps nvcc -> hipcc.
6563
is_rocm = torch.version.hip is not None
6664

6765
force_cuda = os.getenv("FORCE_CUDA", "0") == "1"
@@ -74,13 +72,6 @@ def get_extensions():
7472
) or force_cuda:
7573
extension = CUDAExtension
7674

77-
if is_rocm:
78-
pulsar_prefix = os.path.join(extensions_dir, "pulsar") + os.sep
79-
sources = [s for s in sources if not s.startswith(pulsar_prefix)]
80-
source_cuda = [
81-
s for s in source_cuda if not s.startswith(pulsar_prefix)
82-
]
83-
8475
sources += source_cuda
8576
define_macros += [("WITH_CUDA", None)]
8677
# Thrust is only used for its tuple objects.

0 commit comments

Comments
 (0)