Repository navigation
perf(rocm): port Metal-only kernels (xielu, add3 norm, mamba1, relu2) #2069
Description
Lowest priority of the #1814 ports. The moe_fc1_relu2 part depends on #2065 (HIP moe_down and the fused_relu2 table guard); the other three parts have no dependency.
Problem / Background
Four kernels have a Metal port only (.cuda = nullptr, .rocm = nullptr), so a HIP port has to be written from Metal source, and each sits behind a support check that does not read its table. They run only on narrow model families (Apertus, Cohere2, Mamba1 and Jamba, and an opt-in Nemotron-H path), which is why they come last.
Current Behavior
All in src/lib/mlxcel-core/cpp/mlx_cxx_kernels.cpp:
| Kernel | Metal source | Table | Dispatch | Gate that ignores the table | Runs on |
|---|---|---|---|---|---|
fused_xielu |
XIELU_METAL_SOURCE :368 |
xielu_ports() :415 |
:478 |
early return to xielu_elementwise when !metal::is_available() (:463) |
Apertus, per layer per token (src/models/apertus.rs:261-266) |
fused_add3_layer_norm |
ADD3_LN_METAL_SOURCE :2814 |
add3_layer_norm_ports() :2910 |
:2949 |
residual_add3_layer_norm requires ffi::metal_is_available() (src/lib/mlxcel-core/src/layers.rs:1125) |
Cohere2, per layer per token |
mamba1_selective_scan |
MAMBA1_SCAN_METAL_SOURCE :1674 |
mamba1_scan_ports() :1737 |
:1804 |
mamba1_scan_kernel_available() (:1751-1762) returns false off Apple and reads MLXCEL_MAMBA1_SCAN_KERNEL only on Apple |
Mamba1, Jamba (src/models/mamba.rs:361, src/models/jamba.rs:560) |
moe_fc1_relu2 |
MOE_FC1_RELU2_METAL_SOURCE :2223 |
moe_fc1_relu2_ports() :2278 |
:2293 |
opt-in MLXCEL_FUSED_MOE_RELU2 only (:1439-1440), which #2065 makes require this table and moe_down_ports() |
Nemotron-H MoE decode, opt-in |
Proposed Solution
Each kernel is independent and may land as its own PR. For each: a *_HIP_SOURCE written from the Metal source (translating simdgroup operations to wave32 shuffles), a HIP holder, a filled .rocm, and the gate change below. .cuda stays nullptr, so CUDA keeps its current fallback.
fused_xielu. Replace the early return's condition with!mlxcel::has_kernel_port(xielu_ports()), and update the comment atapertus.rs:261-263. Parity: new test insrc/models/apertus_tests.rscomparingmlxcel_core::fused_xieluwithapertus_xielu(apertus.rs:136) on random f32, f16 and bf16 inputs, within thefused_norm_parity_tests.rs:57budget (f32 1e-6 / 1e-5, f16 2e-3 / 1.2e-2, bf16 1.6e-2 / 7e-2 normalized RMS / max).fused_add3_layer_norm. New bridge predicatefused_add3_layer_norm_available()returninghas_kernel_port(add3_layer_norm_ports());layers.rs:1125reads it and theexpectmessage at:1157names it. The existing testresidual_add3_layer_norm_matches_the_unfused_pair(layers.rs:9630) asserts byte-identical output against the unfused pair on the same backend, so take the kernel's structure from the Metal source and its reduction order from the overlay'ssrc/lib/mlx-cpp/patches-rocm/mlx/backend/rocm/layer_norm.hip, and cover dims up toFUSED_ADD3_LAYER_NORM_MAX_DIM(6656). If byte identity cannot be reached, stop and report it on this issue; do not loosen the test.mamba1_selective_scan.mamba1_scan_kernel_available()returnsfalsewhenMLXCEL_MAMBA1_SCAN_KERNEL=0on any platform, otherwisehas_kernel_port(mamba1_scan_ports()); remove the#ifdef __APPLE__split. Parity: the existingsrc/lib/mlxcel-core/src/mamba1_scan_parity_tests.rs(f32 against a scalar reference, bf16 no less accurate than the graph scan), which starts running on ROCm.moe_fc1_relu2. HIP port only; the default stays off. Parity: Nemotron-3-Nano-30B-A3B-4bit greedy 128-token output withMLXCEL_FUSED_MOE_RELU2=1matches the default path on gfx1151, as it does on Metal.
Port requirements (the same for every port split from #1814)
Filling a .rocm slot follows bitlinear_matmul (#1862, PR #1870), the one kernel with all three ports, in src/lib/mlxcel-core/cpp/mlx_cxx_kernels.cpp: a HIP source string (:159), a holder that calls mlx::core::fast::hip_kernel(...) under #ifdef MLXCEL_BRIDGE_ROCM_BACKEND and throws otherwise (:214-237), and a .rocm getter in the KernelPorts table (:240-255). fast::hip_kernel compiles through hipRTC at runtime, like cuda_kernel, so there is no ahead-of-time build cost.
- Wave32 guard. gfx1151 is wave32; CDNA parts (gfx90a, gfx942) are wave64, where a fold that starts at lane 16 drops half the lanes and still returns a finite, plausible, wrong result. Every ported kernel that reduces or shuffles across lanes carries both
#if defined(__AMDGCN_WAVEFRONT_SIZE__) && __AMDGCN_WAVEFRONT_SIZE__ != 32plus#errorand the same for the non-underscored__AMDGCN_WAVEFRONT_SIZE, as atmlx_cxx_kernels.cpp:170-175.static_assert(warpSize == 32)does not compile in HIP. Shuffles pass an explicit width of 32. - Dtype keys. Every input dtype is in
template_args, so the hipRTC cache key ("custom_kernel_" + name + template_arguments_hash(template_args),src/lib/mlx-cpp/patches-rocm/mlx/backend/rocm/custom_kernel.cpp:231) differs per dtype.make verify-kernel-dtype-keyspasses withEXPECTED_IN_SCOPE(scripts/ci/check_kernel_dtype_keys.py:111) and the9 in scopecontrol count asserted inscripts/ci/check_kernel_dtype_keys_test.sh:113both unchanged, because the HIP launch stays in the file that already holds the CUDA launch. If a launch has to live in a new file, pin that file and update the count in the same change. - Predicate in the same change. The kernel's support predicate returns
mlxcel::has_kernel_port(<its table>), and every gate in front of the kernel reads that predicate, so the port is reachable and the predicate and the dispatch cannot disagree. The dispatch keepsmlxcel::select_kernel_port, andmake verify-kernel-port-dispatchpasses. - Parity. On gfx1151 the port matches the graph fallback it replaces, on the same inputs, within the tolerance named in this issue, not only against the CUDA arm.
- Measured. Decode (or prefill, where stated) tok/s before and after with
scripts/bench_decode.shon an otherwise idle GPU (90 s with/sys/class/kfd/kfd/procempty and no compiler running, as indocs/benchmark_results/rocm-baseline-gfx1151-2026-09-30.md), compared withscripts/compare_bench_csv.py --before <old.csv> --after <new.csv>, with the numbers in the PR. - Metal and CUDA unchanged. The
.metaland.cudaentries and their sources are untouched, and the predicate still answers true on both.
Scope
In scope: the four HIP ports and the three gate changes above, the fused_xielu parity test.
Out of scope: CUDA ports of these kernels; making MLXCEL_FUSED_MOE_RELU2 a default.
Acceptance Criteria
- All four tables have a
.rocmgetter with the wave32 guard, andgrep -rn '\.rocm = nullptr' --include=*.cpp src/reports four fewer lines. -
grep -n 'metal::is_available' src/lib/mlxcel-core/cpp/mlx_cxx_kernels.cppno longer matches infused_xieluormamba1_scan_kernel_available, andresidual_add3_layer_normno longer callsmetal_is_available. - On gfx1151: the new xielu test,
residual_add3_layer_norm_matches_the_unfused_pair(taking the kernel path) andmamba1_scan_parity_testspass, and the relu2 greedy check matches. - Decode tok/s before and after on gfx1151, GPU otherwise idle, for each family with a checkpoint on the host; any family without one is named in the PR.
-
make verify-kernel-dtype-keysandmake verify-kernel-port-dispatchpass with the pin and its count unchanged; Metal and CUDA behavior unchanged.
Verification
cargo test --release --features rocm -p mlxcel-core --lib -- --test-threads=1 mamba1_scan_parity_tests residual_add3_layer_norm
cargo test --release --features rocm -p mlxcel --lib -- --test-threads=1 apertus
make verify-kernel-dtype-keys verify-kernel-port-dispatch
make verify-rocm
Activity
[-]perf(rocm): port the Metal-only kernels (xielu, add3 norm, mamba1, relu2)[/-][+]perf(rocm): port Metal-only kernels (xielu, add3 norm, mamba1, relu2)[/+]update(rocm): port the fused xIELU kernel to HIP
update(rocm): port the float32-state Mamba1 scan kernel to HIP
update(rocm): port the fused add3 + LayerNorm kernel to HIP
update(rocm): port the fc1 squared-ReLU MoE kernel to HIP
update(rocm): port the fused xIELU kernel to HIP
update(rocm): port the float32-state Mamba1 scan kernel to HIP
update(rocm): port the fused add3 + LayerNorm kernel to HIP
docs(rocm): list the newly ported kernels in the README
fix(rocm): gate #2069 kernel ports on the GPU device
fix(rocm): gate the fused MoE predicates on the GPU device too
docs(rocm): publish the #2069 kernel port results on gfx1151
docs: add technical report for PR #2101
update(rocm): port xIELU, add3 norm, Mamba1 and relu2 kernels to HIP (l…