Skip to content

Add AMD GPU support (ROCm/HIP) - #1

Closed
jeffdaily wants to merge 6 commits into
masterfrom
moat-port
Closed

jeffdaily wants to merge 6 commits into
masterfrom
moat-port

Conversation

@jeffdaily

@jeffdaily jeffdaily commented Aug 8, 2026 •

Copy link
Copy Markdown
Collaborator

This adds AMD GPU support to Marian by porting the CUDA GPU backend to HIP, targeting ROCm. There is no AMD path in the tree today, so this is new hardware support rather than a change to existing behavior. The NVIDIA build is untouched: every change is additive or guarded, and with USE_HIP off the CUDA sources compile exactly as before.

Build integration

A new USE_HIP CMake option selects the HIP path. Marian compiles its GPU sources through the legacy cuda_add_library from FindCUDA, so under USE_HIP that call is satisfied by a shim macro that creates an ordinary library and marks the GPU translation units as HIP, driving CMAKE_HIP_ARCHITECTURES instead of a -gencode list. No architecture is hardcoded; when the option is not set the architecture is auto-detected, and the build stops with a clear message rather than guessing if detection fails.

Source changes

A single compat header aliases the CUDA runtime symbols Marian uses to their HIP equivalents. A small directory of forwarding headers (cuda_fp16.h, cublas_v2.h, cublasLt.h, cusparse.h, curand.h, cooperative_groups.h and friends) sits on the include path for HIP builds only, so the sources keep their CUDA spelling and the diff stays small; on NVIDIA that directory is absent and the real toolkit headers are used. Library mappings are one-to-one: cuBLAS to hipBLAS, cuBLASLt to hipBLASLt, cuSPARSE to hipSPARSE, cuRAND to hipRAND, Thrust to rocThrust.

The one change worth close review

src/tensors/gpu/topk.cu and src/translator/nth_element.cu reduce shared memory with a __syncthreads() tree down to 32 elements and then finish with an unrolled warp-synchronous tail that has no barriers and reads non-volatile shared memory. That is correct only where the hardware warp is 32 lanes wide and executes in lockstep. AMD CDNA parts have a 64-lane wavefront, so the tail's assumption does not hold there: the low 32 lanes are not implicitly synchronized across those steps and non-volatile shared memory may be register-cached, which can make the beam-search argmax silently wrong and non-deterministic. On the HIP path the tree now runs all the way down to a single element under __syncthreads(), preserving the original comparison order.

Because this sits on the beam-search top-k, where a wrong result corrupts translations rather than crashing, it was checked with fixed-seed determinism runs (two runs bit-identical) and a BLEU comparison, not by inspection alone.

Validation

Built and tested on AMD Instinct MI250X (gfx90a, 64-lane wavefront) and Radeon Pro W7800 (gfx1100, 32-lane wavefront) under ROCm 7.2.1 on Linux, and on Radeon RX 9070 XT (gfx1201) on Windows. All 292 targets build on each. The graph, attention and transformer unit suites pass in full (10/10, 6/6 and 3/3 assertions). The operator suite passes 284 of 287 assertions.

The three failing assertions are all in the csr-dot product section and are the honest consequence of a gap described below, not a regression: they exercise the sparse path, which this change does not port. Please say whether you would prefer those cases guarded off when the HIP backend is selected, or left failing so the gap stays visible.

Deliberate scope limits

Sparse matrix multiply (cuSPARSE SpMM) is not ported, which is what the three operator-suite failures above reflect. This path serves sparse-attention and LSH models; the default dense Transformer does not reach it.

Multi-GPU collectives are not ported. USE_NCCL must be OFF for a HIP build; the vendored NCCL submodule does not build under hipcc. RCCL is API-compatible with NCCL and ships in ROCm, so this is a tractable follow-up rather than a dead end.

The cuDNN convolution and pooling path is left CUDA-only. USE_CUDNN is already OFF by default and the affected operators are a legacy character-CNN feature, so a MIOpen port is separable from this change.

One Windows configuration does not work yet, for a reason outside Marian: on gfx1101 a rocBLAS grouped-batched GEMM falls back to a generic kernel image that the Windows loader rejects. The Windows gfx1201 configuration above is unaffected and passes.

Build recipe

cmake -S . -B build-hip -G Ninja \
  -DUSE_HIP=ON -DCOMPILE_CUDA=ON \
  -DUSE_CUDNN=OFF -DUSE_NCCL=OFF -DUSE_FBGEMM=OFF \
  -DCOMPILE_CPU=ON -DCOMPILE_TESTS=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build-hip -j$(nproc)

This pull request was prepared with the help of an AI assistant (Claude) 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.

jeffdaily added 6 commits June 2, 2026 01:49
Adds first AMD GPU support for Marian via a Strategy-A CUDA-to-HIP port behind
option(USE_HIP); the NVIDIA build is unchanged (every divergence is
USE_HIP-guarded). Authored with the assistance of Claude (an AI assistant).

Review order: start with the single compat header
src/tensors/gpu/cuda_to_hip.h (runtime cuda*->hip* aliases plus a
CUDA_VERSION>=11000 sentinel) and the forwarding shims under
src/tensors/gpu/hip_compat/ (cublas_v2/cublasLt/cusparse/curand/fp16/...). Then
the CMake: top-level CMakeLists.txt adds the USE_HIP branch (enable_language
HIP, find the ROCm math libs, a cupoch-style cuda_add_library shim macro that
marks the .cu and mixed .cpp TUs LANGUAGE HIP, force-include the compat header).
Then the library swaps in prod.cpp / prod_sparse_cu11.h and the FP16 steering in
common/types.h + functional/{operators,defs,tensor}.h. Finally the wave64
reduction fix in tensors/gpu/topk.cu and translator/nth_element.cu.

Root cause of the key correctness fix: topk.cu and nth_element.cu reduced shared
memory with a __syncthreads() tree down to 32 lanes, then an UNROLLED
warp-synchronous tail (UNROLL_MAXARG_LOOP 32..1) with no barrier on non-volatile
shared memory. That tail assumes the bottom 32 lanes run in lockstep, which
holds on a 32-wide NVIDIA warp but NOT on the low 32 lanes of a CDNA wave64
wavefront -- so the argmax becomes racy and non-deterministic. nth_element is
the beam-search top-k on the decode path, so this would silently corrupt
translations rather than fail loudly. Fix (USE_HIP-guarded): drop the unrolled
tail and run the __syncthreads()-synchronized tree to s>0. Same compare order, a
block-wide barrier at every step, correct on any wave size; the CUDA path is
byte-identical.

hipBLASLt deltas (USE_HIP-guarded in prod.cpp): the
MATMUL_PREF_MIN_ALIGNMENT_*_BYTES preference attrs do not exist and are dropped;
the matmul desc uses HIPBLAS_COMPUTE_32F and a HIP_R_32F scale type for both
fp16 and fp32 (HIPBLAS_COMPUTE_16F / a 16F scale type are rejected);
cublasSetMathMode(TENSOR_OP) is unsupported on gfx90a so the tensor-op-math
helpers are no-ops; hipBLASLt needs its own handle (CUDA shares one with
cuBLAS). FP16 steering keys on __HIPCC__/__HIP_DEVICE_COMPILE__ rather than a
global __CUDACC__ define (which would misroute rocThrust to its CUDA backend).

Scope: built and validated with USE_CUDNN=OFF and USE_NCCL=OFF -- the cuDNN
convolution/pooling path (all #ifdef CUDNN-gated) is kept CUDA-only (a MIOpen
port is a separable follow-up), and NCCL multi-GPU collectives (RCCL swap) are a
follow-up; neither is needed by the default dense Transformer. The cuSPARSE
sparse-attention CSRProd path is also a documented follow-up (hipSPARSE SpMM
order/alg work).

Test Plan:
Built for gfx90a (ROCm 7.2.1):
```
git submodule update --init src/3rd_party/sentencepiece
cmake -S . -B build-hip -G Ninja -DUSE_HIP=ON -DCMAKE_HIP_ARCHITECTURES=gfx90a \
  -DCMAKE_HIP_COMPILER=/opt/rocm/llvm/bin/clang++ -DCOMPILE_CUDA=ON \
  -DUSE_CUDNN=OFF -DUSE_NCCL=OFF -DUSE_FBGEMM=OFF -DCOMPILE_CPU=ON \
  -DCMAKE_BUILD_TYPE=Release -DCOMPILE_TESTS=ON -DUSE_MKL=OFF \
  -DUSE_TCMALLOC=OFF -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DBUILD_ARCH=native
cmake --build build-hip -j
```
Ran the GPU unit suites on a real gfx90a:
```
./src/tests/units/run_graph_tests        # all pass
./src/tests/units/run_attention_tests    # all pass
./src/tests/units/run_transformer_tests  # all pass
./src/tests/units/run_operator_tests     # 284/287 (only cuSPARSE csr-dot fails)
```
operator_tests covers the topk/argmax/argmin/sort kernels (exact values).
End-to-end beam-search decode (exercises nth_element.cu) on gfx90a: trained a
tiny Transformer to convergence, then
```
marian-decoder -m model.npz -v v.src.yml v.tgt.yml -i test.src -b 6 --devices 0
```
run twice is bit-identical (deterministic -> the wave64 race is gone) and equals
the --cpu-threads 1 decode (correct). gMaxElement/gMaxElementUpdate confirmed
dispatched on gfx90a via AMD_LOG_LEVEL=3.
Enable building on Windows with clang++ targeting x86_64-pc-windows-msvc
(non-MSVC driver, MSVC ABI). Seven concrete changes:

- CMakeLists.txt: replace hardcoded -fPIC with a WIN32-conditional FPIC_FLAG;
  on Windows it is empty (clang rejects -fPIC for the MSVC target triple).
- CMakeLists.txt: add WIN32_UNICODE_FLAGS on Windows (UNICODE/_UNICODE,
  DWIN32/_WINDOWS, -D_DLL -D_MT --dependent-lib=msvcrt) so that
  LoadLibrary() maps to LoadLibraryW and the CRT is picked up.
- CMakeLists.txt: remove the Clang exclusion from -march=native; clang++
  supports -march= fine on all targets.
- CMakeLists.txt: add Shlwapi.lib in the else(MSVC) path under WIN32
  guard; pathie-cpp calls PathMatchSpecW which lives there.
- CMakeLists.txt: add -Wno-string-plus-int / -Wno-unused-private-field to
  ALL_WARNINGS for WIN32+Clang (spdlog Windows API, threadpool.h lambda).
- CMakeLists.txt: add -Wno-string-plus-int to MARIAN_HIP_NO_WARN (same
  spdlog warning fires in HIP TUs).
- src/3rd_party/CMakeLists.txt: guard the Clang -fPIC flags on libyaml-cpp
  and pathie-cpp with NOT WIN32.
- src/3rd_party/sentencepiece/src/CMakeLists.txt: add NOT WIN32 guard on
  the -fPIC flag in the if(NOT MSVC) block.
- src/3rd_party/sentencepiece/src/trainer_interface.cc: change constexpr to
  const for kAnyType (Clang strict constexpr rejects out-of-range enum cast).

These changes are guarded by WIN32 and/or CMAKE_CXX_COMPILER_ID matches
"Clang" conditions; the Linux and MSVC paths are byte-identical.

This work was authored with Claude (Anthropic).

Test Plan:
cmake -S . -B build-gfx1201-win -G Ninja \
  -DUSE_HIP=ON -DCMAKE_HIP_ARCHITECTURES=gfx1201 \
  -DCMAKE_C_COMPILER=<rocm_devel>/lib/llvm/bin/clang.exe \
  -DCMAKE_CXX_COMPILER=<rocm_devel>/lib/llvm/bin/clang++.exe \
  -DCMAKE_HIP_COMPILER=<rocm_devel>/lib/llvm/bin/clang++.exe \
  -DCOMPILE_CUDA=ON -DUSE_CUDNN=OFF -DUSE_NCCL=OFF \
  -DUSE_FBGEMM=OFF -DCOMPILE_CPU=ON -DCMAKE_BUILD_TYPE=Release \
  -DCOMPILE_TESTS=ON -DUSE_MKL=OFF -DUSE_TCMALLOC=OFF -DUSE_DOXYGEN=OFF \
  -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DBUILD_ARCH=native \
  -DOpenBLAS_LIBRARIES=<rocm_devel>/lib/host-math/lib/rocm-openblas.lib \
  -DOpenBLAS_INCLUDE_DIRS=<rocm_devel>/lib/host-math/include/openblas
cmake --build build-gfx1201-win -j32
# Runs successfully, 20 link targets produced.
# GPU tests: graph_tests 10/10 PASS on gfx1201 (RDNA4, RX 9070 XT).
On Windows with TheRock ROCm 7.14, libhipblaslt.dll faults inside Tensile's
FP8 type check (hipblaslt_f8::is_inf) whenever marian's Affine() drives the
fused GEMM+bias through the hipblasLtMatmul descriptor API
(hipblasLtMatmulAlgoGetHeuristic). The crash is independent of the input data
type (it triggers for fp32 and fp16 alike) and wedges every Affine-bearing
path: the operator "affine transformation" test, attention, transformer, and
end-to-end train/decode.

The fix routes Affine on Windows + ROCm around the hipBLASLt epilogue: compute
the matmul with the plain GEMM (ProdTyped, which lowers to hipblasGemmEx /
hipblasSgemm) and then add the broadcast bias with the existing BiasAdd kernel,
applying the same optional ReLU epilogue. This is exactly the pre-CUDA-11
fallback already present in this file, so the numerical result is identical to
the hipBLASLt epilogue. The change is guarded by _WIN32 && USE_HIP; Linux and
CUDA keep the hipBLASLt path byte for byte (the post-call misalignment BiasAdd
that belongs to the hipBLASLt path is likewise guarded off on Windows + ROCm
since the new affineTyped already applies the bias).

This was developed with the assistance of Claude, an AI assistant.

Test Plan:

Built for gfx1201 (RX 9070 XT, RDNA4) with the TheRock all-clang Windows
toolchain and ran the marian GPU unit tests, pinned to one GPU:

```
cmake --build agent_space/marian-build-gfx1201 -j24
HIP_VISIBLE_DEVICES=0 ROCBLAS_USE_HIPBLASLT=0 \
  run_operator_tests.exe "Expression graph supports basic math operations (gpu)"
HIP_VISIBLE_DEVICES=0 ROCBLAS_USE_HIPBLASLT=0 run_attention_tests.exe "*Attention (gpu)"
HIP_VISIBLE_DEVICES=0 ROCBLAS_USE_HIPBLASLT=0 \
  run_transformer_tests.exe "Expression graph supports basic math operations (gpu)"
```

operator_tests (gpu): 200/202 assertions pass; the affine transformation
section passes (previously SIGSEGV). attention (gpu): 2/2 pass. transformer
(gpu): 1/1 pass. The 2 remaining operator failures are the pre-existing
deferred cuSPARSE csr-dot path, unrelated to this change. A tiny Transformer
trained and beam-search decoded on gfx1201 (Affine in the FFN forward and
backward) converges and is run-to-run deterministic.
Drop internal project labels from the ROCm/HIP shim comments and the
CMake status message while keeping the technical descriptions intact.
Comment-only change; authored by Claude.
Replace "byte-identical" in three port comments with plainer, accurate
wording. The CUDA path is left unchanged when USE_HIP=OFF; "byte-identical"
overclaimed and was not the right description. No code logic changes.

Authored with assistance from Claude.

Test Plan:
Comment-only change; no build or test impact.
The ROCm/HIP path force-set CMAKE_HIP_ARCHITECTURES to gfx90a before
enable_language(HIP) whenever it was unset, which preempted CMake's own
host-GPU detection: a build on a non-gfx90a AMD GPU that did not pass an
explicit -DCMAKE_HIP_ARCHITECTURES silently produced gfx90a code objects
that fail to load at runtime.

Removing the pin lets enable_language(HIP) honor an explicit
-DCMAKE_HIP_ARCHITECTURES, otherwise auto-detect the host GPU(s), and
otherwise error on a host with no AMD GPU.

Test Plan:
Configured and built with an explicit
-DCMAKE_HIP_ARCHITECTURES=gfx90a; the produced device code objects are
byte-identical to the previous pinned build (the pin only set the same
value the -D now supplies).

Authored with the Claude AI assistant.
@jeffdaily

Copy link
Copy Markdown
Collaborator Author

To approve this port, leave a review comment containing this line by itself:

/moat approve

Use Review changes -> Comment, or gh pr review https://github.com/AMD-Ecosystem/marian-dev/pull/1 --comment --body '/moat approve'. It has to be a review rather than an ordinary comment, because a review records which commit you were looking at, and that is what proves the approval covers this code and not an earlier push.

The title and body above are what gets opened upstream, verbatim, so approving here approves all three: the code, the title and the body. Anything pushed afterwards, or any edit to the title or body, voids it and needs a fresh one.

jeffdaily added a commit to AMD-Ecosystem/moat that referenced this pull request Aug 8, 2026
@jeffdaily

Copy link
Copy Markdown
Collaborator Author

The body has been updated and now carries the standing submission note: that the change was prepared with an AI assistant and approved by a person, a link back to the repository this work comes from, and how a maintainer can ask not to receive pull requests from it. That note replaces the one-line AI-assistance disclosure this body already ended with, and it is now added automatically whenever a review PR is opened, so it is part of what gets approved and what gets published verbatim.

Nothing else changed. The code, the title and the rest of the body are as they were, and no commit has been pushed since the platforms validated at ba0ec80.

This PR carried no approval yet, so nothing was voided. To approve it, leave a review comment containing this line by itself:

/moat approve

Use Review changes -> Comment, or gh pr review https://github.com/AMD-Ecosystem/marian-dev/pull/1 --comment --body '/moat approve'. It has to be a review rather than an ordinary comment, because a review records which commit you were looking at, and that is what proves the approval covers this code and not an earlier push.

The title and body above are what gets opened upstream, verbatim, so approving here approves all three: the code, the title and the body. Anything pushed afterwards, or any edit to the title or body, voids it and needs a fresh one.

@jeffdaily jeffdaily left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reject this port as it is today. All three deliberate scope limits were not signed off by a me. The SpMM, NCCL, and cuDNN features should be ported, not deferred.

@jeffdaily

Copy link
Copy Markdown
Collaborator Author

Submitted upstream as marian-nmt#1043 with the title and body approved here. Closing this review PR; the discussion continues on the upstream one.

@jeffdaily jeffdaily closed this Aug 10, 2026
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.

1 participant