Repository navigation
docs(rocm): document MLX_ROCM_GATHER_QMV_EXPERT_BATCHED and the other runtime MLX_ROCM_* variables #2180
Description
Part of #1801
Problem / Background
MLX_ROCM_GATHER_QMV_EXPERT_BATCHED switches the expert-batched gather_qmm kernel, which has been on by default for bf16 and f16 affine since #2112 (#2066) and for bf16 mxfp4 since #2164 (#2106). A user who wants the per-row path back, or who is bisecting a MoE regression, can only find the variable in src/lib/mlx-cpp/patches-rocm/LOCAL_FIXES.md (items 9 and 31), two benchmark pages, a code comment and a test. It is absent from docs/environment-variables.md and from the ROCm section of docs/installation.md. The same holds for 42 other MLX_ROCM_* variables the overlay reads at runtime.
Current Behavior
grep -rhoE 'MLX_ROCM_[A-Z0-9_]+' src/lib/mlx-cpp/patches-rocmfinds 51 names.docs/environment-variables.mdnames 7:MLX_ROCM_ARCHITECTURES,MLX_ROCM_FFT_CACHE_SIZE,MLX_ROCM_MAX_INFLIGHT_MB,MLX_ROCM_GPU_WATCHDOG_SECS,MLX_ROCM_WMMA_QMM,MLX_ROCM_WMMA_QMM_MAX_M,MLX_ROCM_QMM_DEQUANT_GEMM, all as prose inside "Build-time variables" (docs/environment-variables.md:179-233).MLX_ROCM_HOST_STAGEis a compile definition (mlx/backend/rocm/CMakeLists.txt:458), not an environment variable, and stays undocumented. The other 43 are runtime variables with no documentation.docs/installation.md"Linux with AMD ROCm" (:366-574) covers onlyMLX_ROCM_ARCHITECTURES,MLX_ROCM_MAX_INFLIGHT_MB,MLX_ROCM_GPU_WATCHDOG_SECSandMLX_ROCM_FFT_CACHE_SIZE; its "Runtime environment variables" table (:575-591) has no ROCm rows.- Existing text is wrong for one variable:
docs/environment-variables.md:225says the dequantize + hipBLASLt route is enabled when "MLX_ROCM_QMM_DEQUANT_GEMMnot0", butuse_rocblas_dequant_path()(quantized/qmm.hip:480-492) enables it only when the variable is unset or exactly1; any other value, includingtrueor an empty string, disables it.
Proposed Solution
- In
docs/environment-variables.md, add a section## ROCm backend variablesdirectly after "Build-time variables". Move the existing runtime ROCm prose (FFT cache, in-flight bound, watchdog, WMMA QMM, WMMA QMM max M, dequant GEMM) into it unchanged except for theMLX_ROCM_QMM_DEQUANT_GEMMcorrection, and leave one sentence plus a link in "Build-time variables". Under it add a table with columnsVariable | Values | Default | Purpose, the same as "Hardware and kernel diagnostic variables", with one row per variable below, grouped in this order: MoE gather qmv, dense qmv, dequantize + GEMM, GEMM libraries, unquantized MoE, allocator and device. State in the section intro that every path is undersrc/lib/mlx-cpp/patches-rocm/mlx/backend/rocm/, that most variables are read once per process and the ones marked "every call" can be changed between calls, and that "presence" variables take effect for any value, including0. - In
docs/installation.md, add one paragraph to the ROCm "Running" subsection namingMLX_ROCM_GATHER_QMV_EXPERT_BATCHED=0as the way to take the per-row MoE path for A/B or bisecting, linking to the new section, and add one row to the "Runtime environment variables" table that points to it. - Take every value and default from the code at implementation time, not from this table, if the code has moved. fix(rocm): range-check GEMM env integers and fix a stale HIP graph comment #2152 (open) changes the GEMM integer parsers to warn on invalid input; if it merges first, document its behavior.
| Variable | Read at | Values | Default | Effect |
|---|---|---|---|---|
MLX_ROCM_GATHER_QMV_EXPERT_BATCHED |
quantized/qmm.hip:6242 |
0 off, 1 on, else default; every call |
on | gather_qmv_expert_batched_kernel for a sorted, transposed gather_qmm with M == 1, B >= 64, E <= 64, B / E >= 4: bf16 or f16 affine 4/8-bit at group 64, and bf16 mxfp4. 0 sends every scheme to the unsorted-path kernels. |
MLX_ROCM_GATHER_QMV_USE_WARP |
quantized/qmm.hip:6099 |
0/1; every call |
on for transposed 2/4/8-bit and affine 5/6-bit | Gates the fast gather qmv kernels (wide, idot, warp-shared); 0 sends gather_qmm to the per-row gather_qmv_kernel. |
MLX_ROCM_GATHER_QMV_THREADS_PER_COL |
quantized/qmm.hip:6071 |
16 or the wave size, else ignored |
select_qmv_threads_per_col (16; the wave size at K >= 16384 with batch 1 on wave32) |
Lanes per output column in the fast gather kernels; unset falls back to MLX_ROCM_QMV_THREADS_PER_COL. |
MLX_ROCM_GATHER_QMV_USE_TILED |
quantized/qmm.hip:6318 |
presence | off | Opt-in tiled gather qmv for bf16 affine 4/8-bit group 64 with a unit-stride 1-D batch. |
MLX_ROCM_GROUPED_PREFILL |
quantized/qmm.hip:6110 |
integer, nonzero enables | off | Work-in-progress grouped WMMA prefill for bf16 affine 4-bit group 64 gather_qmm; the code notes it regresses. |
MLX_ROCM_GROUPED_PREFILL_MIN_B |
quantized/qmm.hip:6114 |
integer (atoi, unvalidated) |
64 | Minimum B for the grouped prefill. |
MLX_ROCM_GATHER_QMV_USE_WMMA |
quantized/qmm.hip:6123 |
0/1 |
native WMMA and N % 16 == 0 |
Only inside the grouped prefill; 0 falls back to the standard dispatch. |
MLX_ROCM_QMV_USE_WARP |
quantized/qmm.hip:3778 |
0/1 |
on when transposed | 0 disables the warp-shared qmv for quantized_matmul, except where the batched qmv applies (forced on). |
MLX_ROCM_QMV_THREADS_PER_COL |
quantized/qmm.hip:3791 |
16 or the wave size |
as the gather row | Lanes per output column in the dense qmv. |
MLX_ROCM_QMV_COLS_PER_BLOCK |
quantized/qmm.hip:506 |
4, 8, 16, 32, 64, else ignored; every call |
select_qmv_cols_per_block (by N, K, bits) |
Output columns per block for the dense qmv, the gather warp-shared and the expert-batched kernels. |
MLX_ROCM_QMV_TILE_N |
quantized/qmm.hip:3846 |
integer (atoi, unvalidated) |
per-arch qmv_tile_n, 4 to 24 (device/config.h:169-186) |
Columns per block of the tiled qmv, halved until it divides N. |
MLX_ROCM_QMV_NO_TILED |
quantized/qmm.hip:3851 |
presence | tiled on | Disables the L2-tiled qmv. |
MLX_ROCM_QMV_6BIT_SLOW |
quantized/qmm.hip:3855, :6327 |
presence | off | Sends 6-bit dense and gather qmv back to the warp-shared kernel. |
MLX_ROCM_FORCE_LOW_CU |
quantized/qmm.hip:591 |
value starting with 1 |
devices with 8 CUs or fewer | Treats the device as a low-CU iGPU for qmv and WMMA tuning. |
MLX_ROCM_QMM_DEQUANT_M_THRESHOLD |
quantized/qmm.hip:792 |
positive integer, else ignored | heuristic | quantized_matmul takes dequantize + GEMM exactly when M >= the value. |
MLX_ROCM_QMM_DEQUANT_CACHE_SIZE |
quantized/qmm.hip:983 |
non-negative integer | 8 | Entries in the LRU cache of dequantized weights; 0 disables it. |
MLX_ROCM_QMM_DEQUANT_CACHE_MAX_BYTES |
quantized/qmm.hip:999 |
non-negative integer, bytes | 268435456 (256 MiB) | Byte cap of that cache; 0 disables it. |
MLX_ROCM_GEMM_F32_SOLUTION_INDEX |
matmul.cpp:194, gemms/rocblas_gemm.cpp:54, quantized/qmm.hip:1005 |
non-negative integer | 0 (rocBLAS standard algorithm) | rocBLAS solution index for single f32 GEMMs. |
MLX_ROCM_GEMM_F32_BATCHED_SOLUTION_INDEX |
same files | non-negative integer | the single f32 index | Same, for batched f32 GEMMs. |
MLX_ROCM_GEMM_BF16_SOLUTION_INDEX |
matmul.cpp:205, gemms/rocblas_gemm.cpp:65, quantized/qmm.hip:1016 |
non-negative integer | 0 | rocBLAS solution index for single bf16 GEMMs. |
MLX_ROCM_GEMM_BF16_BATCHED_SOLUTION_INDEX |
same files | non-negative integer | the single bf16 index | Same, for batched bf16 GEMMs. |
MLX_ROCM_GEMM_BF16 |
gemms/hipblaslt_gemm.cpp:259 |
first character 1, o or O |
off | Forces bf16 instead of fp8 e4m3 for half-precision GEMMs on devices whose hipBLASLt has fp8 kernels (RDNA4). true does not enable it. |
MLX_ROCM_NO_HIPBLASLT_EPILOGUE |
gemms/hipblaslt_gemm.cpp:378 |
presence | epilogue on | Disables the hipBLASLt bias epilogue that fuses a Linear bias into the GEMM. |
MLX_ROCM_HIPBLASLT_TUNE |
gemms/hipblaslt_gemm.cpp:768 |
presence | off | Times every returned hipBLASLt algorithm per shape and keeps the fastest; costs warm-up time. |
MLX_ROCM_GEMM_DEBUG |
gemms/hipblaslt_gemm.cpp:1252 |
presence | off | Prints each hipBLASLt GEMM's shape, transposes and epilogue to stderr. |
MLX_ROCM_SORTED_GATHER |
matmul.cpp:1205 |
exactly 0 disables |
on | Collapses consecutive same-expert rows of an M == 1 gather_mm into one tiled GEMM per run. |
MLX_ROCM_MOE_SEG_MIN |
matmul.cpp:1215 |
integer >= 1, else ignored |
1 | Minimum average rows per expert run for segment GEMMs instead of the gemv gather. |
MLX_ROCM_MOE_PACK |
matmul.cpp:1309 |
first character 0, f, F, n, N disables |
on | Pack plus strided-batched hipBLASLt for bf16 1-D MoE gather_mm, padded rows, no device-to-host copy. |
MLX_ROCM_MOE_DEVICE_SEG |
matmul.cpp:1317 |
first character 1, o, O, t, T |
off | VALU device-segment path instead of the pack. |
MLX_ROCM_MOE_PACK_EXACT |
matmul.cpp:1323 |
as above | off | Legacy exact max-run through a device-to-host copy (synchronizes). |
MLX_ROCM_MOE_ASYNC |
matmul.cpp:1491 |
as above | off | Host path inside hipLaunchHostFunc; the code warns it can deadlock on some ROCm builds. |
MLX_ROCM_MOE_NO_PIN_CACHE |
matmul.cpp:1551 |
as above | off | Stops reusing the pinned host copy of the sorted indices across the gate, up and down calls. |
MLX_ROCM_MOE_PIN_STATS |
matmul.cpp:1582 |
presence | off | Prints [moe-pin] hits=... misses=... to stderr every 200 calls. |
MLX_ROCM_MOE_PAD_SYNC |
gemms/naive_gemm.hip:2034 |
first character 0, n, N, f, F: heuristic only; a, A, 2: measure every call; else cache |
cache (one sync per (T, E)) |
Padded row count for the MoE pack GEMMs (matmul.cpp:1369, moe_swiglu.cpp:144, :576); heuristic-only may read out of bounds under extreme imbalance (gemms/naive_gemm.h:144-146). |
MLX_ROCM_MOE_ZERO_SYNC |
moe_swiglu.cpp:133, :548 |
first character 0, f, F, n, N disables |
on | Fused sorted-MoE SwiGLU forward and VJP use pack plus batched hipBLASLt with no device-to-host copy; off uses host run-length segments. |
MLX_ROCM_MOE_VJP_DEVICE_SEG |
moe_swiglu.cpp:556 |
first character 1, o, O, t, T |
off | VJP uses VALU token tiles and segmented weight gradients. |
MLX_ROCM_FINEGRAINED |
allocator.cpp:95 |
integer, nonzero on | on for an integrated GPU, off for discrete | Fine-grained host-coherent device memory. |
MLX_ROCM_USE_ASYNC_POOL |
allocator.cpp:112 |
presence | off | Stream-ordered hipMallocAsync pool. |
MLX_ROCM_FORCE_ASYNC_POOL |
allocator.cpp:113, :351 |
presence | off | As MLX_ROCM_USE_ASYNC_POOL, and also creates the pool on gfx1201, which is skipped by default. |
MLX_ROCM_NO_ASYNC_POOL |
allocator.cpp:110 |
presence | unset | Forces the pool off; wins over the two above. |
MLX_ROCM_ALLOW_MANAGED_FALLBACK |
allocator.cpp:181 |
presence | off (integrated GPUs always allow it) | On a discrete GPU, retries a failed hipMalloc with hipMallocManaged instead of throwing. |
MLX_ROCM_NO_MANAGED_FALLBACK |
allocator.cpp:183 |
presence | unset | Forbids that fallback on every device; a failed hipMalloc throws. |
MLX_ROCM_FORCE_WARP_SIZE |
device.cpp:147 |
32 or 64, else ignored with a stderr line |
the device's wavefront size | Overrides the host-side launch width; debug only, a wrong value produces garbage output. |
Scope
In scope: docs/environment-variables.md (new section, the 43 rows, the moved prose, the MLX_ROCM_QMM_DEQUANT_GEMM correction) and docs/installation.md (one paragraph, one table row).
Out of scope: code changes to any parser (#2152 covers the GEMM integers); the overlay's undocumented non-MLX_ROCM_ variables (MLX_GRAPH_*, MLX_QMV_IDOT, MLX_QMV_IDOT_GATHER, MLX_QMV_NO_WIDE, MLX_SDPA_*, MLX_HIPBLASLT_*, MLX_NO_HIPBLASLT, MLX_HSACO_CACHE_DIR, MLX_GRIDX_MULT, MLX_NO_CONCAT_SPLIT, MLX_PURE_DEBUG, MLX_COUNT_COPIES), which need their own issue.
Implementation Notes
- Reuse: the table layout of "Hardware and kernel diagnostic variables" (
docs/environment-variables.md:516); the markdown conventions already in that file. - Constraints: describe what the code does today; do not mark a variable recommended for deployment. Mark
MLX_ROCM_FORCE_WARP_SIZE,MLX_ROCM_MOE_ASYNC, heuristic-onlyMLX_ROCM_MOE_PAD_SYNCandMLX_ROCM_GROUPED_PREFILLas debug or experimental, as their code comments do. - Edge cases: for presence variables, say outright that
=0enables; for first-character parsers, list the accepted characters, not words.
Acceptance Criteria
-
docs/environment-variables.mdhas a## ROCm backend variablessection with all 43 rows, each with values, default and effect matching the code, and the six runtime variables already documented in it. - The comparison in Verification prints only
MLX_ROCM_HOST_STAGE. - The
MLX_ROCM_QMM_DEQUANT_GEMMtext says only unset or exactly1enables the route. -
docs/installation.md's ROCm section namesMLX_ROCM_GATHER_QMV_EXPERT_BATCHED=0and links to the new section, and its runtime-variable table points to it. - Internal links resolve (anchors checked by hand or with the docs build).
Verification
comm -23 <(grep -rhoE 'MLX_ROCM_[A-Z0-9_]+' src/lib/mlx-cpp/patches-rocm/mlx src/lib/mlx-cpp/patches-rocm/CMakeLists.txt | sort -u) <(grep -oE 'MLX_ROCM_[A-Z0-9_]+' docs/environment-variables.md | sort -u)
grep -n 'MLX_ROCM_GATHER_QMV_EXPERT_BATCHED' docs/installation.md docs/environment-variables.mdA pass: the first command prints MLX_ROCM_HOST_STAGE and nothing else; the second finds the variable in both files.
Activity
fix(rocm): range-check GEMM env integers and fix a stale graph comment (
docs(rocm): document the runtime ROCm backend variables (lablup#2222)