FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Add ROCm/HIP support for GPU SIFT feature extraction and matching by jeffdaily · Pull Request #4635 · colmap/colmap · GitHub

/ colmap Public

Add ROCm/HIP support for GPU SIFT feature extraction and matching - #4635

Open
jeffdaily wants to merge 3 commits into
colmap:mainfrom
AMD-Ecosystem:moat-port
Open

Add ROCm/HIP support for GPU SIFT feature extraction and matching#4635
jeffdaily wants to merge 3 commits into
colmap:mainfrom
AMD-Ecosystem:moat-port

Conversation

Copy link
Copy Markdown
Contributor

COLMAP's HIP backend so far covered dense reconstruction only (added in #4420), so a ROCm build silently fell back to CPU or GLSL SIFT for feature extraction and matching, which is the most-used GPU path in a typical reconstruction. This change builds SiftGPU's compute backend on ROCm and widens the call sites that decide whether the GPU path is taken.

Suggested review order. Read the build wiring first: SiftGPU's CMakeLists gains an arm that compiles the same sources under the HIP toolchain and declares the same rocRAND dependency the CUDA arm already declares for cuRAND; GPU_ENABLED learns about HIP_ENABLED (it gates the SiftGPU subdirectory, colmap_feature's link, and COLMAP_GPU_ENABLED); and colmap_controllers links the GPU libraries on HIP as it already did on CUDA. The sources stay in the CUDA spelling; src/colmap/util/cuda_to_hip.h, the existing compatibility header, gains the handful of symbols SiftGPU needs and remains the only file naming a hipXxx symbol. Then read the three fixes in SiftGPU, then the widened conditionals, which are mechanical.

Three latent faults surfaced on AMD hardware. Two are backend-independent and are fixed for both CUDA and HIP, because the bug is not specific to the platform that happened to expose it.

CuTexObj owned a texture object handle with no default member initializer and allowed implicit copies. ProgramCU default-constructs one, copy-assigns a binding into it, and the temporary's destructor then destroys the handle the survivor still holds, which it destroys again at scope exit; on the existing-keypoint path an uninitialized handle is passed to a kernel and destroyed. It is now move-only, zero-initialized, and its destructor is guarded.

ComputeDOG_Kernel read index-1, index+1, index-width and index+width without clamping. The texture is a linear binding, where the address mode does not apply, so at the image border those really were out-of-range fetches. The stencil is now clamped to the image.

The gradient image was bound as a pitched 2D texture whose row pitch is the tightly packed image row. AMD GPUs require a 256-byte texture pitch, so hipCreateTextureObject rejects any pyramid level narrower than that: a 640x480 input fails at the 80-wide float2 level. All three fetches are point-filtered and already clamp their coordinates, so a point-sampled pitched fetch is exactly a linear fetch at row * width + column; the binding is now linear, which has no pitch requirement. This is the only one of the three that is purely a platform fix, and it is unconditional because it keeps one code path.

RunThreadWithOpenGLContext was an empty inline without Qt, which made every caller's body a silent no-op. Eight call sites route through it (seven in src/colmap/exe/feature.cc and one in src/colmap/exe/sfm.cc), as well as the GPU tests in sift_test, so a headless build reported the whole suite passing while not executing a single GPU test body. The executables are affected in principle rather than in practice, because FindDependencies.cmake turns OPENGL_ENABLED off when the GUI is off and the feature extractor then defaults use_gpu to false, but this is not test-only code. It now runs the thread and waits for it, a GPU test needs a GPU rather than a window, and opengl_utils_test gains a case that asserts the body ran.

The legacy cudaGL* pixel-buffer-object interop in CuTexImage has no ROCm counterpart and is compiled out there. COLMAP always supplies host pixel data and never reaches those branches, which are only used by SiftGPU's own standalone viewer, but SiftGPU is vendored source that other consumers build, so each compiled-out entry point now reports its failure rather than returning silently. CuTexImage::CopyFromPBO returns void into a buffer its caller has just allocated, so a silent no-op left the pyramid reading uninitialized device memory as if it were the image; it now writes a message to stderr and clears the buffer.

Test plan

Built and tested on an AMD Radeon Pro W7800 (gfx1100) and an AMD Instinct MI250X (gfx90a) with ROCm 7.2 on Linux, and on an AMD Radeon 8060S (gfx1151) with ROCm 7.13 on Windows.

cmake -S . -B build-hip-gui -GNinja \
    -DCUDA_ENABLED=OFF -DHIP_ENABLED=ON \
    -DCMAKE_HIP_ARCHITECTURES=gfx1100 \
    -DCMAKE_BUILD_TYPE=Release \
    -DTESTS_ENABLED=ON -DGUI_ENABLED=ON \
    -DCGAL_ENABLED=OFF -DDOWNLOAD_ENABLED=OFF -DONNX_ENABLED=OFF
cmake --build build-hip-gui -j"$(nproc)"
xvfb-run -a ctest --test-dir build-hip-gui -j4 --output-on-failure

On Linux, 159 of 159 tests pass on both GPUs, including feature/sift_test, mvs/gpu_mat_test and util/opengl_utils_test. sift_test is 32 of 32, of which 15 are GPU tests, including MatchSiftFeaturesCPUvsGPU.Nominal and MatchGuidedSiftFeaturesCPUvsGPUGuided.EssentialMatrix, which compare the GPU result against the CPU result in-suite.

The GPU test bodies were confirmed to execute rather than be skipped, by checking that the SiftGPU kernels actually launch:

AMD_LOG_LEVEL=3 xvfb-run -a ./src/colmap/feature/sift_test

On gfx1100 that reports FilterH x31, FilterV x31, ComputeDOG x30, ReduceHist x45, RowMatch x25, ColMatch x24, ComputeKEY x18, InitHist x18, ListGen x14, MultiplyDescriptor x9, ComputeOrientation x5 and ComputeDescriptor x5 dispatches. Wall time is not a reliable substitute here: in a build without the GUI the GPU matcher tests take a few milliseconds each, which looks like a skipped body, and only the dispatch list distinguishes the two.

End to end on the MI250X, on a three-image set at 640x480 and at 1024x768 (the first being the case that used to fail the 256-byte texture pitch check), feature_extractor plus exhaustive_matcher produce 948 to 1047 keypoints per image and 3 of 3 verified image pairs at the lower resolution, and 3062 to 3124 keypoints and 3 of 3 at the higher one.

On Windows (gfx1151) with a hardware OpenGL context, the GPU compute path passes: gpu_mat_test 4 of 4, the GPU SIFT matching tests 31 of 31, opengl_utils_test 3 of 3, and 1530 assertions total across the suite, with the descriptor kernels dispatching and returning results confirmed by AMD_LOG_LEVEL=3.

The CUDA build was compile-checked and not run: no NVIDIA GPU was available, so no CUDA result was measured and no numerical comparison against CUDA was made.

cmake -S . -B build-cuda -GNinja \
    -DCUDA_ENABLED=ON -DHIP_ENABLED=OFF \
    -DCMAKE_CUDA_ARCHITECTURES=80 \
    -DCMAKE_BUILD_TYPE=Release \
    -DTESTS_ENABLED=ON -DGUI_ENABLED=ON \
    -DCGAL_ENABLED=OFF -DDOWNLOAD_ENABLED=OFF -DONNX_ENABLED=OFF
cmake --build build-cuda --target colmap_sift_gpu colmap_mvs_cuda \
    colmap_feature_sift_test colmap_main -j"$(nproc)"

That is nvcc 13.3.73, and it compiles and links clean. Two of the three fixes are unconditional, so they change what the CUDA build computes as well, and the argument that they are safe there is an argument rather than a measurement. The ComputeDOG clamp changes the first and last row and column of every difference-of-Gaussian level, which is where the old code read the previous row's last pixel or past the end of the buffer; every interior pixel is unchanged, since the clamped indices reduce to the old index plus or minus 1 and index plus or minus width there. The pitched-to-linear rebind is exact rather than approximate: all three fetches use point filtering, the pitch was the packed image row, InitTexture allocates packed, and all three kernels clamp x into [1.5, width - 1.5] and y into [1.5, height - 1.5] before fetching, so hardware addressing never applies and truncation equals floor.

Known issue

On Windows, ExtractSiftFeaturesGPU crashes during OpenGL buffer teardown after the GPU computation itself has completed successfully. This is a pre-existing GL-context lifecycle issue in code this change does not modify (the SiftGPU pixel-buffer teardown and OpenGLContextManager), it does not affect the GPU computation, and it does not reproduce on Linux. It is filed separately as #4633.


This pull request was prepared with the help of an AI assistant acting as a coding agent and was read and approved by a person before it was opened. It comes from an ongoing effort to add AMD GPU support to widely used CUDA projects, one repository at a time: https://github.com/AMD-Ecosystem/moat -- that repository describes how the work is done and what a person checks before anything is submitted.

If you would rather not receive pull requests from this effort, say so here or open an issue at https://github.com/AMD-Ecosystem/moat/issues/new/choose and we will close this and stop. That can cover this repository alone or everything you own, whichever you prefer.

COLMAP's HIP backend so far covered dense reconstruction only, so a ROCm build
silently fell back to CPU or GLSL SIFT for feature extraction and matching --
the most-used GPU path in a typical reconstruction. This builds SiftGPU's
compute backend on ROCm and widens the call sites that decide whether the GPU
path is taken.

Read the build wiring first: SiftGPU's CMakeLists gains an arm that compiles the
same sources under the HIP toolchain and declares the same rocRAND dependency
the CUDA arm already declares for cuRAND, GPU_ENABLED learns about HIP_ENABLED
(it gates the SiftGPU subdirectory, colmap_feature's link and
COLMAP_GPU_ENABLED), and colmap_controllers links the GPU libraries on HIP as it
already did on CUDA. The sources stay in the CUDA spelling;
src/colmap/util/cuda_to_hip.h, the existing compatibility header, gains the
handful of symbols SiftGPU needs and remains the only file naming a hipXxx
symbol. Then read the three fixes in SiftGPU, then the widened conditionals,
which are mechanical.

Three latent faults surfaced on AMD hardware. Two are backend-independent and
are fixed for both, because the bug is not specific to the platform that
happened to expose it:

CuTexObj owned a texture object handle with no default member initializer and
implicit copies. ProgramCU default-constructs one, copy-assigns a binding into
it, and the temporary's destructor then destroys the handle the survivor still
holds, which it destroys again at scope exit; on the existing-keypoint path an
uninitialized handle is passed to a kernel and destroyed. It is now move-only,
zero-initialized, and its destructor is guarded.

ComputeDOG_Kernel read index-1, index+1, index-width and index+width without
clamping. The texture is a linear binding, where the address mode does not
apply, so at the image border those really were out-of-range fetches. The
stencil is now clamped to the image.

The gradient image was bound as a pitched 2D texture whose row pitch is the
tightly packed image row. AMD GPUs require a 256-byte texture pitch, so
hipCreateTextureObject rejects any pyramid level narrower than that: a 640x480
input fails at the 80-wide float2 level. All three fetches are point-filtered
and already clamp their coordinates, so a point-sampled pitched fetch is exactly
a linear fetch at row * width + column; the binding is now linear, which has no
pitch requirement. This is the only change of the three that is purely a
platform fix, and it is unconditional because it keeps one code path.

RunThreadWithOpenGLContext was an empty inline without Qt, which made every
caller's body a silent no-op. Eight call sites route through it, seven of them
in src/colmap/exe/feature.cc and one in src/colmap/exe/sfm.cc, as well as the
GPU tests in sift_test, so a headless build reported the whole suite passing
while not executing a single GPU test body. The executables are affected in
principle rather than in practice, because FindDependencies.cmake turns
OPENGL_ENABLED off when the GUI is off and the feature extractor then defaults
use_gpu to false, but this is not test-only code. It now runs the thread and
waits for it, a GPU test needs a GPU rather than a window, and opengl_utils_test
gains a case that asserts the body ran.

The legacy cudaGL* pixel-buffer-object interop in CuTexImage has no ROCm
counterpart and is compiled out there. COLMAP always supplies host pixel data
and never reaches those branches, which are only used by SiftGPU's own
standalone viewer, but SiftGPU is vendored source that other consumers build, so
each compiled-out entry point reports its failure rather than returning
silently. The two that return a value already did. CuTexImage::CopyFromPBO
returns void into a buffer its caller has just allocated, so a silent no-op left
the pyramid reading uninitialized device memory as if it were the image; it now
writes a message to stderr and clears the buffer.

This was written with the assistance of Claude, an AI assistant by Anthropic.

Test Plan:

Built and tested on an AMD Radeon Pro W7800 (gfx1100) and on an AMD Instinct
MI250X (gfx90a), both with ROCm 7.2.

```
cmake -S . -B build-hip-gui -GNinja \
    -DCUDA_ENABLED=OFF -DHIP_ENABLED=ON \
    -DCMAKE_HIP_ARCHITECTURES=gfx1100 \
    -DCMAKE_BUILD_TYPE=Release \
    -DTESTS_ENABLED=ON -DGUI_ENABLED=ON \
    -DCGAL_ENABLED=OFF -DDOWNLOAD_ENABLED=OFF -DONNX_ENABLED=OFF
cmake --build build-hip-gui -j"$(nproc)"
xvfb-run -a ctest --test-dir build-hip-gui -j4 --output-on-failure
```

159 of 159 tests pass on both GPUs, including feature/sift_test, mvs/gpu_mat_test
and util/opengl_utils_test. sift_test is 32 of 32, of which 15 are GPU tests,
including MatchSiftFeaturesCPUvsGPU.Nominal and
MatchGuidedSiftFeaturesCPUvsGPUGuided.EssentialMatrix, which compare the GPU
result against the CPU result in-suite.

The GPU test bodies were confirmed to execute rather than be skipped, by
checking that the SiftGPU kernels actually launch:

```
AMD_LOG_LEVEL=3 xvfb-run -a ./src/colmap/feature/sift_test
```

On gfx1100 that reports FilterH x31, FilterV x31, ComputeDOG x30, ReduceHist
x45, RowMatch x25, ColMatch x24, ComputeKEY x18, InitHist x18, ListGen x14,
MultiplyDescriptor x9, ComputeOrientation x5 and ComputeDescriptor x5
dispatches. Wall time is not a reliable substitute here: in a build without the
GUI the GPU matcher tests take a few milliseconds each, which looks like a
skipped body, and only the dispatch list distinguishes the two.

End to end on the MI250X, on a three-image set at 640x480 and at 1024x768, the
first being the case that used to fail the 256-byte texture pitch check,
feature_extractor plus exhaustive_matcher produce 948 to 1047 keypoints per
image and 3 of 3 verified image pairs at the lower resolution, and 3062 to 3124
keypoints and 3 of 3 at the higher one.

The CUDA build was compile-checked and NOT run: neither machine has an NVIDIA
GPU, so no CUDA result was measured and no numerical comparison against CUDA was
made.

```
cmake -S . -B build-cuda -GNinja \
    -DCUDA_ENABLED=ON -DHIP_ENABLED=OFF \
    -DCMAKE_CUDA_ARCHITECTURES=80 \
    -DCMAKE_BUILD_TYPE=Release \
    -DTESTS_ENABLED=ON -DGUI_ENABLED=ON \
    -DCGAL_ENABLED=OFF -DDOWNLOAD_ENABLED=OFF -DONNX_ENABLED=OFF
cmake --build build-cuda --target colmap_sift_gpu colmap_mvs_cuda \
    colmap_feature_sift_test colmap_main -j"$(nproc)"
```

That is nvcc 13.3.73, and it compiles and links clean.

Two of the three fixes are unconditional, so they change what the CUDA build
computes as well, and the argument that they are safe there is an argument
rather than a measurement. The ComputeDOG clamp changes the first and last row
and column of every difference-of-Gaussian level, which is where the old code
read the previous row's last pixel or past the end of the buffer; every interior
pixel is unchanged, since the clamped indices reduce to the old index +/- 1 and
index +/- width there. The pitched-to-linear rebind is exact rather than
approximate: all three fetches use point filtering, the pitch was the packed
image row, InitTexture allocates packed, and all three kernels clamp x into
[1.5, width - 1.5] and y into [1.5, height - 1.5] before fetching, so hardware
addressing never applies and truncation equals floor.

Copy link
Copy Markdown
Contributor

Two issues from review:

  1. GUI=OFF, CUDA=OFF, HIP=OFF now segfaults in feature/sift_test: the non-Qt RunThreadWithOpenGLContext() executes all RunGpuTest bodies, but those tests do not skip when no GPU backend exists. I reproduced the crash in ExtractSiftFeaturesGPU.Nominal; util/opengl_utils_test passes.

  2. Installed HIP builds still do not export HIP_ENABLED from cmake/colmap-config.cmake.in. Standalone pycolmap uses find_package(colmap), so it cannot reliably recreate the HIP dependency targets needed by the Python paths changed here.

Making RunThreadWithOpenGLContext run the thread without Qt turned the GPU test
bodies in feature/sift_test from silent no-ops into code that actually executes,
which is the point, but it also made them execute in a build that has no GPU
backend at all. With GUI_ENABLED=OFF, CUDA_ENABLED=OFF and HIP_ENABLED=OFF,
OpenGL is disabled too, so COLMAP_GPU_ENABLED is not defined,
CreateSiftFeatureExtractor and CreateSiftFeatureMatcher return nullptr for
use_gpu, and the first such test dereferences it: ExtractSiftFeaturesGPU.Nominal
segfaults.

The tests are not applicable to that configuration, so RunGpuTest now skips
instead of running, which is also what a reader expects from a suite that is
green on a machine with no GPU support compiled in. Every GPU test in the file
goes through RunGpuTest, so this is the one place that needs to know. Nothing
changes for a build with OpenGL, CUDA or HIP: the thread runs and the bodies
execute exactly as before.

This was written with the assistance of Claude, an AI assistant by Anthropic.

Test Plan:

The reported configuration, which segfaulted before this change and is now 18
passed and 14 skipped in feature/sift_test and 158 of 158 in the whole suite:

```
cmake -S . -B build-nogpu -GNinja \
    -DCUDA_ENABLED=OFF -DHIP_ENABLED=OFF -DGUI_ENABLED=OFF \
    -DCMAKE_BUILD_TYPE=Release \
    -DTESTS_ENABLED=ON -DCGAL_ENABLED=OFF -DDOWNLOAD_ENABLED=OFF \
    -DONNX_ENABLED=OFF
cmake --build build-nogpu -j"$(nproc)"
ctest --test-dir build-nogpu -j8 --output-on-failure
```

util/opengl_utils_test passes there as well, both before and after.

The ROCm configuration was rebuilt to confirm it still compiles, with ROCm 7.14
targeting gfx90a:

```
cmake -S . -B build-hip -GNinja \
    -DCUDA_ENABLED=OFF -DHIP_ENABLED=ON \
    -DCMAKE_HIP_ARCHITECTURES=gfx90a \
    -DCMAKE_BUILD_TYPE=Release \
    -DTESTS_ENABLED=ON -DGUI_ENABLED=OFF -DCGAL_ENABLED=OFF \
    -DDOWNLOAD_ENABLED=OFF -DONNX_ENABLED=OFF
cmake --build build-hip -j"$(nproc)"
```

The GPU test results reported earlier, on an AMD Radeon Pro W7800 (gfx1100) and
an AMD Instinct MI250X (gfx90a), are unaffected: the skip is compiled out
wherever a GPU backend exists.
An installed ROCm build exports colmap_util_cuda, colmap_mvs_cuda and
colmap_sift_gpu, and their link interfaces name hip::host and roc::rocrand. The
package config does not set HIP_ENABLED, so the FindDependencies.cmake it
includes takes its no-HIP path, those imported targets are never created, and
find_package(colmap) fails at generate time with

    The link interface of target "colmap::colmap_sift_gpu" contains:
      hip::host
    but the target was not found.

That is how the standalone Python bindings consume COLMAP, so a ROCm build could
not be used to build them, which is precisely the configuration the bindings
changed here need. HIP_ENABLED is now exported next to CUDA_ENABLED, and the
ROCm root the library was built against travels with it so that a consumer finds
the same installation even when it is not in the default location; an explicit
ROCM_PATH from the consumer still wins, since the cache entry is only a default.

This was written with the assistance of Claude, an AI assistant by Anthropic.

Test Plan:

Configure, build and install a ROCm build into a scratch prefix, with ROCm 7.14
targeting gfx90a:

```
cmake -S . -B build-hip -GNinja \
    -DCUDA_ENABLED=OFF -DHIP_ENABLED=ON \
    -DCMAKE_HIP_ARCHITECTURES=gfx90a \
    -DCMAKE_BUILD_TYPE=Release \
    -DTESTS_ENABLED=ON -DGUI_ENABLED=OFF -DCGAL_ENABLED=OFF \
    -DDOWNLOAD_ENABLED=OFF -DONNX_ENABLED=OFF \
    -DCMAKE_INSTALL_PREFIX=/tmp/colmap-install
cmake --build build-hip -j"$(nproc)"
cmake --install build-hip
```

The installed share/colmap/colmap-config.cmake now carries "set(HIP_ENABLED ON)"
and the ROCm root. A minimal consumer, which is what the Python bindings do,

```
cmake_minimum_required(VERSION 3.21)
project(colmap_consumer LANGUAGES C CXX)
find_package(colmap REQUIRED)
add_executable(consumer main.cc)
target_link_libraries(consumer PRIVATE colmap::colmap)
```

fails to configure against the installed tree without this change with the error
quoted above, and with it configures, builds and links, reporting COLMAP_HIP_ENABLED
defined in the consumer's own translation unit.

jeffdaily commented Aug 17, 2026
edited
Loading

Copy link
Copy Markdown
Contributor Author

Note: this reply was drafted by an AI assistant.

Both addressed.

  1. Reproduced the segfault with GUI=OFF, CUDA=OFF, HIP=OFF (ExtractSiftFeaturesGPU.Nominal, 0 of 32 cases complete). RunGpuTest is the choke point all 14 GPU tests go through, so it now does GTEST_SKIP when no GPU backend is compiled in, matching the skip style used elsewhere in the tree. After the fix that build reports 18 passed / 14 skipped in sift_test and 158/158 across the suite. The RunThreadWithOpenGLContext change itself is kept: the previous empty non-Qt inline silently turned the GPU test bodies into no-ops that reported PASS on headless CUDA builds as well, which seemed worth keeping fixed.

  2. colmap-config.cmake.in now exports HIP_ENABLED next to the CUDA export, and carries ROCM_PATH as a cache default so find_package(colmap) consumers resolve ROCm even when it is not at /opt/rocm; an explicit -DROCM_PATH= still overrides. Verified with a minimal find_package(colmap) consumer against an installed HIP build: without the export it fails at generate time ("link interface ... contains: hip::host but the target was not found"), with it the consumer configures, builds, and sees COLMAP_HIP_ENABLED.

Re-ran the full test suite at the new head on two AMD GPUs (Radeon Pro W7800 and Instinct MI250X, Linux): 159/159 pass, and the SIFT kernel dispatch counts match the previous runs exactly.

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants


Back | FazBrowse Home | New Git URL