perf(rocm): profile gfx1151 decode per kernel and rank the #1814 ports - #2086
Conversation
Implementation Review SummaryIntent
Findings Addressed
Checked, no change neededEvery figure in the doc's tables matches Verification
|
Security and performance reviewNo CRITICAL or HIGH findings, so nothing was changed on the branch. The scripts are local operator tooling: every input (model paths, flags, Checked and clean: commands are passed as argv arrays ( MEDIUM
LOW
|
The three grouped_gemm_numeric_tests gated on Metal or CUDA, so on ROCm they returned before touching the GPU and MLX's ROCm GatherMM was never checked. Run by exact name on gfx1151 with the gate widened, all three pass against the f64 host reference, and a rocprofv3 kernel trace shows the overlay's gather_batched_gemm_kernel (f32, bf16, f16) and a hipBLASLt GEMM for the sorted single-row case, so the pass is not vacuous. With the reference pointed at the wrong expert, all three fail at the value assertion. The gates now read gpu_backend_available(), the file leaves BACKEND_ENUMERATION_TODO in check_kernel_port_dispatch.py, and the checker reports 0 awaiting a predicate. Metal and CUDA still run the tests (not run here). Refs #2061
ROCm had end-to-end tok/s only, so the #1814 kernel ports had no measured order. This adds what a per-kernel decode profile needs, as reusable tooling rather than a one-off: - scripts/rocm_gpu_guard.sh: the idle-GPU guard the #2056 baseline described, as a script (90 s with /sys/class/kfd/kfd/proc empty and no compiler, a 1 Hz monitor that ignores the command's own GPU processes, rerun on contention, every sample logged). - mlxcel-bench-decode: --temperature and --top-p (default greedy, unchanged), and MLXCEL_BENCH_PHASE_MARKS=1, which prints the warmup, measured, decode-start and end times on CLOCK_MONOTONIC and CLOCK_BOOTTIME so a trace can be cut to the measured decode by timestamp. - scripts/rocm_decode_profile.sh runs a plain and a rocprofv3 --kernel-trace --hip-graph-trace --stats run per model under the guard; scripts/rocm_decode_profile.py cuts the decode window, reports GPU time and host gap per token, profiler cost, top kernels, and attributes dispatches to mlxcel ops and #1814 port units by dispatch order, with which ports mlxcel actually reaches per model. - tests/test_rocm_decode_profile.py covers the guard against a fake KFD directory and the cut and attribution rules on synthetic steps; docs/benchmarks.md documents the harness. Refs #2061
Profiles greedy and sampled decode (pp512/tg128) of Llama-3.1-8B, Qwen3-30B-A3B, granite-4.0-h-tiny and Nemotron-3-Nano-30B-A3B on gfx1151 with rocprofv3, every run under the idle-GPU guard (16 accepted runs; one rejected by the guard, one discarded and rerun by hand), and attributes decode GPU time to the port units split from #1814. Shipped-settings share of decode GPU time: #2067 SSM update 29.8% (granite) and 20.0% (Nemotron) plus more than half their dispatches; #2065 fused MoE 46.8% of Qwen3, mostly GEMVs already at about 181 GB/s; #2064 samplers 0 greedy, 0.4 to 3.8% sampled; #2063 zero (both fusions ship off) and #2068 zero (no paged path in single-stream decode). Implied order: 7, 5, 4, 8, 3. The rocprofv3 stats CSVs, per-kernel decode tables, summaries, bench logs and the guard log are under benchmarks/rocm_profiles/gfx1151_929c80ab/. The report also records the gather_mm test outcome. Refs #2061
The decode window holds 127 forward passes (the first of 128 tokens comes from the prefill), while per-token figures divide by 128 to match the bench's tok/s. The attribution check quoted 46.6 and 49.6 SSD dispatches per Mamba2 layer and claimed whole multiples "per token"; per step they are exactly 47 and 50, which is the check that actually holds. The Method section now states the 127/128 relation. Also: the #2063 opt-in ceiling is 0.83 to 0.89% (the top-p run reads 0.89), the #2064 reached share counts the whole sampler tail including the --ignore-eos logit bias, so it is slightly high rather than net of it, and a guard-rejected attempt stays in the run's _bench.log (granite greedy) while only the last, accepted attempt is read. Numbers recomputed from the committed summaries and decode CSVs; no GPU run. Refs #2061
rocm_decode_profile.sh now scrubs local paths from the published guard.log from an EXIT trap, so an early exit (guard 75, rocprofv3 or cp failure) no longer leaves them in, and the replacement is literal so '#' and regex characters in the paths are harmless. rocm_decode_profile.py makes Dispatch slotted to bound the memory of a full kernel trace; the report output for benchmarks/rocm_profiles/gfx1151_929c80ab is byte-identical. rocm_gpu_guard.sh reads the parent pid after the last ')' of /proc/<pid>/stat, avoids glob expansion when splitting holders, rejects non-integer --idle-secs, --max-attempts and --max-wait (and a missing option value) with exit 2, and stops the command and monitor on INT or TERM (exit 130 or 143). Tests cover the validation, octal-looking values, the SIGTERM path and slots. Validation: python3 -m unittest tests/test_rocm_decode_profile.py (17 pass), bash -n on both scripts, make verify-fmt verify-kernel-port-dispatch. No GPU workload was run. Refs #2061
c9d6811 to
0d5d6db
Compare
Measures where ROCm decode time goes per kernel on gfx1151 and ranks the port issues split from #1814 by it; settles MLX's ROCm
gather_mm.What changed
docs/benchmark_results/rocm-decode-profile-gfx1151-2026-09-30.md: decode GPU time and host gap per token, top kernels, share per port unit for Llama-3.1-8B, Qwen3-30B-A3B, granite-4.0-h-tiny and Nemotron-3-Nano (greedy, plus temperature and top-p runs), the attribution method, and the ranked order. Raw rocprofv3 stats CSVs, per-kernel decode tables, summaries and the guard log are underbenchmarks/rocm_profiles/gfx1151_929c80ab/.scripts/rocm_gpu_guard.sh(the perf(bench): ROCm support in the benchmark harness and a gfx1151 baseline #2056 idle-GPU guard as a script),scripts/rocm_decode_profile.sh/.py(profile, decode cut, attribution, report),mlxcel-bench-decode --temperature/--top-pandMLXCEL_BENCH_PHASE_MARKS=1; documented indocs/benchmarks.md.grouped_gemm_numeric_tests.rsgates readgpu_backend_available(); the file leftBACKEND_ENUMERATION_TODO.Result: implied order #2067, #2065, #2064, #2068, #2063 (items 7, 5, 4, 8, 3). Every measured run passed the guard (90 s idle, 1 Hz monitor); one attempt was rejected by the guard, one rerun by hand. Profiler cost scales with dispatch count: within noise on Llama, 5% on Qwen3, 20 to 23% on the hybrids, so the doc reports shares.
Verification (gfx1151)
cargo test --release --features rocm -p mlxcel-core --lib grouped_gemm_numeric_tests -- --test-threads=1 --nocapture: 3 passed; each also run by exact name under rocprofv3, trace showsgather_batched_gemm_kerneland a hipBLASLt GEMM; all three fail with a wrong-expert referencepython3 scripts/ci/check_kernel_port_dispatch.py:0 awaiting a predicate;make verify-kernel-port-dispatchpassesmake verify-versions verify-kernel-dtype-keys verify-kernel-port-dispatch verify-llama-compat verify-fmt: passcargo clippy -p mlxcel --features rocm --bin mlxcel-bench-decode -- -D warningsandcargo clippy -p mlxcel-core --features rocm --lib --tests -- -D warnings: clean (narrow scope instead ofmake verify-clippy-rocm, which is workspace-wide)cargo test --features rocm --test dead_doc_pointers: passpython3 -m unittest tests/test_rocm_decode_profile.py: 17 passed;bash -non all three scripts (shellcheck is not installed on this host)Changes during review
rocm_decode_profile.shscrubs local paths fromguard.login an EXIT trap with a literal replacement;rocm_gpu_guard.shparses the parent PID after the last), avoids glob expansion, validates its integer options, and stops the command and monitor on INT/TERM;Dispatchinrocm_decode_profile.pyis slotted.rocm_decode_profile.py reportoutput on the committed data is byte-identical before and after.Not verified: Metal and CUDA (not available here). The
gather_mmgate change means the three tests still run there, unchanged.cargo test --test dead_doc_pointerswithout--features rocmfails to link on this host (copy_gpu_inplaceundefined fromkv_inplace_write.cpp), unrelated to this change.Closes #2061