diff --git a/TECHNICAL_REPORTS/2218-rocm-joint-rejection-cap-20261008.en.md b/TECHNICAL_REPORTS/2218-rocm-joint-rejection-cap-20261008.en.md new file mode 100644 index 000000000..2802e9a41 --- /dev/null +++ b/TECHNICAL_REPORTS/2218-rocm-joint-rejection-cap-20261008.en.md @@ -0,0 +1,159 @@ +# Technical Report: PR #2218 - Recalibrating the Joint Top-k + Top-p Rejection Vocab Cap on ROCm + +**Date**: 2026-10-08 + +**Status**: Implemented and verified on the gfx1151 host (ROCm/HIP); code head `7d737d49` rebased on main `ae343d84`, PR open, pending merge. Closes #2157, part of #1801. + +**Languages**: C++ (`mlx_cxx_bridge.cpp`, `mlx_cxx_bridge.h`), Rust (`lib.rs`, `sampling_rejection_tests.rs`, `examples/rejection_sampling_microbench.rs`, `src/bin/bench_decode.rs`), shell (`scripts/bench_decode.sh`), Markdown (benchmark page, `installation.md`, `environment-variables.md`) + +**Risk Level**: Low. Only the value of one routing constant changes, and only in ROCm builds: a ROCm build now sends top-k together with top-p to the rejection kernel up to vocab 152064 instead of 32768. `MLXCEL_SAMPLING_REJECTION=0` restores the stock chain. Metal and CUDA builds keep 32768, but that arm was not run on this host. + +## Executive Summary + +The rejection sampler routes top-k together with top-p only up to `REJECTION_JOINT_VOCAB_MAX`, which was 32768. That number was measured on M1 Ultra, where joint top-k + top-p won 1.27x to 1.64x at 32K and lost (0.71x to 0.83x) at 152K. The HIP port (#2064) inherited it without a measurement. On ROCm, `--top-k 40 --top-p 0.95` on a vocab-128256 model such as Llama 3.1 therefore never reached the kernel. + +On gfx1151 the microbenchmark alone would have kept 32768. Its batch 4 and 8 cells win at every vocabulary (pipelined 1.05x to 2.02x), but the synthetic batch-1 cell reads 0.90x to 0.98x pipelined above 32768. The end-to-end decode run, which the issue names as the confirmation at 128256 and above, disagrees with that cell. With the joint case routed, Meta-Llama-3.1-8B-Instruct-4bit (128256) goes from 32.67 to 37.57 tok/s (1.15x) and Qwen2.5-7B-Instruct-4bit (152064) from 41.91 to 46.92 tok/s (1.12x), medians of three interleaved guarded pairs, with every kernel run faster than every chain run. On the final branch build, on the engine decode path, Llama goes from 33.15 to 37.73 tok/s (1.14x). + +The ROCm value is now 152064, the largest vocabulary measured both ways, chosen by a build flag with no runtime backend branch. A new bridge accessor reports the active value and a test pins it per build. Why the synthetic batch-1 cell does not predict real decode was not isolated. Vocabularies above 152064 were not measured and are unchanged. Metal and CUDA were not verified. The orchestrator's `make verify-rocm` on the rebased head passes (161 suites, 12,189 passed, 0 failed, 398 ignored). + +## 1. Problem Statement + +### 1.1 A cap calibrated on another GPU + +`REJECTION_JOINT_VOCAB_MAX` in `src/lib/mlxcel-core/cpp/mlx_cxx_bridge.cpp` is read by `sampling_rejection_routes` once top-p is active: a top-k that can bind routes only if `vocab <= REJECTION_JOINT_VOCAB_MAX`. The reason top-k loses at large vocabularies on M1 Ultra is that it needs more rejection rounds as the vocabulary grows, and each round is a full-row sweep on one threadgroup. gfx1151 has a different per-threadgroup bandwidth and launch cost, so the crossover can sit elsewhere. Nobody had measured it: `rocm-samplers-gfx1151-2026-10-05.md` noted that `--top-k 40 --top-p 0.95` on a vocab-128256 model never reaches the kernel and that whether the cap is right on ROCm was not measured. + +### 1.2 The microbenchmark could not run on ROCm + +The tool that measures the matrix, `examples/rejection_sampling_microbench.rs`, gated on `custom_kernels_available()`. That function is true on Metal or CUDA by definition, so on a ROCm build the benchmark refused to run. The port that made the kernel run on ROCm never had its crossover measured because the measuring tool was not ported with it. Its vocabulary list also lacked 128256 (Llama 3) and 151936 (Qwen3). + +### 1.3 No way to benchmark the joint filter end to end + +`mlxcel-bench-decode` and `scripts/bench_decode.sh` had no `--top-k` option, so the case the issue was opened for could not be timed through the normal decode benchmark. + +## 2. Change Summary + +| Area | Change | +|---|---| +| `mlx_cxx_bridge.cpp` | `REJECTION_JOINT_VOCAB_MAX` is 152064 under `#ifdef MLXCEL_BRIDGE_ROCM_BACKEND` and 32768 otherwise. The gfx1151 tables are in the comment above it. The not-routed message names the build's cap. | +| `mlx_cxx_bridge.h`, `lib.rs` | New accessor `sampling_rejection_joint_vocab_max()`. | +| `sampling_rejection_tests.rs` | The cap is pinned per build; the routing matrix covers 65536, 128256, 151936 and 152064 per build; the large-vocabulary decline test picks a vocabulary above the build's cap. | +| `examples/rejection_sampling_microbench.rs` | Vocabularies 128256 and 151936 added; `--dtype f32\|bf16\|f16` and `--config