Date: 2026-07-29 · Hardware: 2× MI210 (gfx90a/CDNA2), 64 GB HBM2e each, 499 GB DDR4
Every number below is measured on this box. Raw data: benchmarks/matrix/results/*.json
Mechanisms: docs/24 (quantization), docs/26 (checkpoint selection), docs/27 (matrix cores)
Prefill is tokens/s at a cold 15k prompt. Decode is tokens/s at ~101k context unless noted. Load is wall-clock to first served request.
| Want | Run |
|---|---|
| Best all-round | RedHatAI/*-quantized.w8a8 on vLLM at TP=2 |
| Biggest model worth running | Qwen3-235B-A22B UD-Q3_K_XL — 104 GB fits, 698 t/s |
| Smallest that still flies | AWQ-Int4 on vLLM, TP=1 |
| Long context (>128k) | GGUF Q8_0 / Q4_K_M on llama.cpp |
| Any expert layers on CPU | a K-quant — I-quants lose 36% decode there |
| Never | FP8, gptq-packaged MoE, compressed-tensors 4-bit with type:"int", HSA_XNACK unified memory |
| Weights | Engine | TP | GiB/rank | Load | Prefill | Decode | Verdict |
|---|---|---|---|---|---|---|---|
W8A8 RedHatAI/...quantized.w8a8 |
vLLM | 2 | 14.7 | 60 s | 7,278 | 43.4 | ★ best |
bf16 Qwen/Qwen3-30B-A3B-Thinking-2507 |
vLLM | 2 | 28.5 | 12,366 s | 7,578 | 62.6 | faster, unusable load |
| W8A8 | vLLM | 1 | 29.2 | 123 s | 4,739 | 33.8 | ★ best single-card |
AWQ-Int4 QuantTrio/...AWQ |
vLLM | 1 | 15.7 | 115 s | 3,002 | 17.2 | ★ secondary — smallest |
| GGUF Q8_0 | llama.cpp | — | ~31 GB | — | 3,326 | 29.1 @230k | ★ long context |
| GGUF Q4_K_M | llama.cpp | — | ~18 GB | — | 3,416 | — | secondary long ctx |
FP8 Qwen/...FP8 |
vLLM | 1 | 29.2 | 119 s | 1,047 | 9.3 | ✗ avoid |
bf16 is the trap here. It wins throughput — +4% prefill, +44% decode — and
costs 206× the load time: 3.4 hours against 60 seconds, same model, same
cards. For anything that restarts more than about twice a month that is not a
close call. docs/25 traces the cause to hsakmt_ioctl in the bf16 loader path.
AWQ-Int4 is the memory pick: 15.7 GiB, comfortably one card, and still 3× faster than FP8.
| Weights | Engine | TP | GiB/rank | Load | Prefill | Decode | Verdict |
|---|---|---|---|---|---|---|---|
W8A8 RedHatAI/Qwen3-Next-80B-A3B-Instruct-quantized.w8a8 |
vLLM | 2 | 38.3 | 143 s | 7,253 | 45.2 | ★ best prefill |
W8A16 cyankiwi/...AWQ-8bit |
vLLM | 2 | 41.0 | 184 s | 6,679 | 51.3 | ★ best decode |
AWQ-Int4 cyankiwi/...AWQ-4bit |
vLLM | 1 | 45.4 | 160 s | 4,487 | 45.9 | ★ single-card |
| GGUF Q4_K_M | llama.cpp | — | ~46 GB | — | 2,264 | 53.8 @230k | ★ long context |
This tier has a genuine split. W8A8 takes prefill by 8.5%; W8A16 takes decode by 14%. Pick by workload — long prompts and short answers favour W8A8, long generations favour W8A16.
Two non-obvious facts about these checkpoints, neither visible in the repo name:
- The W8A8 build has its MTP head stripped (0 MTP tensors vs 4,625 in the
cyankiwi builds). Irrelevant today — speculation loses on this hardware
(
docs/25) — but it is a real capability difference. head_dim = 256, so this model reaches no ROCm attention fast path: no AITER ASM, no custom paged attention, no benefit from the 256k patch. It is still the fastest thing in the matrix, entirely on architecture (docs/27).
| Weights | TP | GiB/rank | Load | Prefill | Decode |
|---|---|---|---|---|---|
RedHatAI/Llama-3.3-70B-Instruct-quantized.w8a8 |
2 | 33.9 | 421 s | 843 | 6.8 @64k |
843 t/s against the 80B MoE's 7,253 — 8.6× slower on a smaller model.
Dense 70B activates all 70B parameters per token; Qwen3-Next activates 3B. Both
checkpoints are correct W8A8 at head_dim = 128, and this one loads the AITER
ASM kernels the 80B cannot.
Active parameters dominate everything else. Fast paths are a multiplier on top; they are not a substitute. If you are choosing between a dense model and a sparse MoE of similar size on this box, the MoE wins by roughly the ratio of active parameters.
Use dense models when you need their quality or ecosystem, not for throughput.
| Weights | Engine | On GPU | Prefill | Decode |
|---|---|---|---|---|
GGUF UD-IQ2_M unsloth/GLM-4.6-GGUF |
llama.cpp | 135.02 GB | 217.8 | 10.85 @25.8k |
GGUF IQ3_XS bartowski/... |
llama.cpp | 135.57 GB | 195.9 | 8.51 @25.8k |
AWQ-Int4 bullpoint/GLM-4.6-AWQ |
vLLM + --cpu-offload-gb 70 (UVA) |
— | — | ✗ unusable |
| AWQ-Int4, same weights | vLLM + --offload-backend prefetch --enforce-eager |
131.16 | 695.9 | see below |
| AWQ-Int4, same weights | vLLM + --offload-backend prefetch, graphs on |
— | ✗ NCCL crash | — |
vLLM's UVA offload is not viable at this tier — one 28k request ran past 35 minutes at steady 505% CPU. llama.cpp does the same work at 8.5 t/s.
That result was previously written up here as "vLLM's CPU offload is not viable", which claims more than was measured. vLLM 0.23 has two offload backends and only one was tested:
| Flag | Backend | Mechanism |
|---|---|---|
--cpu-offload-gb |
UVAOffloader |
Weight stays in pinned host memory; the GEMM reads it across PCIe as it runs. No staging, no overlap. |
--offload-group-size |
PrefetchOffloader |
Weight is copied to a static GPU buffer pool on a separate stream, double-buffered, layer N+1 landing while layer N computes. |
The one that was measured is the one that cannot overlap. --offload-params experts further restricts either backend to the expert stacks, which on GLM-4.6
is ~168 GB of the 176 GB.
The prefetch backend works, but only with --enforce-eager. At 75% offload
it first crashed during load with NCCL error: unhandled cuda error in
ncclAllReduce (worker TP1), then hung with the container still alive. A control
run of t35-w8a8 at TP=2 loaded normally on the same GPUs minutes later — 130 s,
41.88 GiB KV — proving it was the offloader, not the hardware.
--enforce-eager fixes it, which also identifies the cause. PrefetchOffloader
splices a private torch.cuda.Stream into CUDA-graph captures by design — its
docstring says events "allow the copy stream to join CUDA graph captures" — and
it patches module forwards to insert wait_prefetch/start_prefetch ops into
the captured graph. RCCL collectives inside that capture are what break.
Removing graph capture removes the collision. So the feature is not broken on
ROCm; it is incompatible with graph capture there, which is a more useful fact.
--offload-backend prefetch --offload-params experts \
--offload-group-size 4 --offload-num-in-group 3 \
--enforce-eager # REQUIRED — without it, NCCL dies at loadWhat is and is not accelerated in that result. Verified from the .co load
lines, not inferred:
| AITER component | State |
|---|---|
| ASM flash attention (prefill) | active — fwd_hd128_bf16_rtna_group.co loaded, matching GLM-4.6's head_dim=128 |
| Custom paged attention (decode) | active — pa_v1 JIT at head_size=128, gqa_ratio=12 |
| AITER linear / GEMM | off — VLLM_ROCM_USE_AITER_LINEAR=0 |
| AITER MoE | off — VLLM_ROCM_USE_AITER_MOE=0 |
So 695.9 t/s is with ASM attention but stock GEMM and stock MoE, leaving
headroom. Note fmoe — the highest MACs/instruction kernel on this box
(docs/33) — is doubly unavailable to this arm: disabled by env, and all 8
gfx90a objects are noquant{Fp16,Bf16}, so they cannot consume AWQ-Int4 weights.
This section previously read: "the startup log says
Unknown vLLM environment variable detected: VLLM_PREFER_AITER_FA. AITER FA is still selected, but on backend priority order rather than by the flag."Backwards on both counts. That flag is the only thing that selects AITER FA on this box, and backend priority order is precisely what would prevent it.
_get_backend_priorities()inplatforms/rocm.pyis a plain ordered list whose first valid entry wins, and it appendsROCM_ATTNunconditionally, first.ROCM_AITER_FAcan only ever be second, so it is reached only whenROCM_ATTNis invalid — which it essentially never is.configs/prefer_aiter_fa_gfx90a.pyis what moves it ahead, gated onVLLM_PREFER_AITER_FA=1, which our patchedrocm.pyreads fromos.environdirectly. That is why the flag works despite vLLM's own envs registry not knowing the name: theUnknown vLLM environment variablewarning is expected and does not mean the flag is inert.The log quoted below shows
['ROCM_AITER_FA', 'ROCM_ATTN', ...]— AITER first. That ordering does not occur upstream; it is the patch.How this was caught. Round 32 built an image without that patch and measured the difference:
['ROCM_ATTN', 'ROCM_AITER_FA', 'TRITON_ATTN'],ROCM_ATTNselected, and zeroLoadKernellines — AITER admitted and never used, whileis_aiter_attention_supported()answeredTruethroughout. Candidacy is not selection, and only the.coload line distinguishes them.
Found incompatible backend(s) [TURBOQUANT] with AttentionType.DECODER.
Overriding with ROCM_AITER_FA out of potential backends:
['ROCM_AITER_FA', 'ROCM_ATTN', 'TRITON_ATTN']
Set VLLM_PREFER_AITER_FA=1 and verify the .co line. Its absence does
mean AITER FA is off. The +8% @15k / +33% @101k this doc once credited to the
flag was a real measurement, and the flag does cause it — but on a build
carrying prefer_aiter_fa_gfx90a.py, which not every image here has.
vLLM has no managed-memory path at all — zero references to
hipMallocManaged/cudaMallocManaged in the package. So the llama.cpp
unified-memory arms (round 15) and this are not the same mechanism: that one
is OS demand paging, page-granular and unscheduled; this one is explicit,
tensor-granular and prefetched.
QuantTrio/Qwen3-235B-A22B-Instruct-2507-AWQ is the only large checkpoint found
that satisfies the whole fast-path profile in docs/35: head_dim 128,
64 heads / 4 KV = GQA ratio 16 (one of exactly two ratios the ASM pa
kernels exist for), no kv_lora_rank, bfloat16, 116 GB, unpruned. Every GLM-5.2
variant fails that profile structurally because MLA routes elsewhere.
Both round-23 arms timed out at 3600 s without serving:
WARNING [awq_marlin.py:316] Layer 'model.layers.93.mlp.experts' is not supported
by AWQMoeMarlin. Falling back to Moe WNA16 kernels.
Loading safetensors checkpoint shards: 8% | 2/25 [43:32<8:33:11, 1338.78s/it]
~9.3 hours projected. Marlin declines the expert layers — it is CUDA-only
here — and AWQ then lands on the same moe_wna16_weight_loader that makes
gptq MoE catastrophic. The 12.5%-offload arm failed identically, as expected:
offload changes where weights live, not how they arrive.
Two secondary observations from that log: the arm omitted
--safetensors-load-strategy=prefetch, which docs/25 documents as mandatory on
btrfs (it would not have saved this — storage delivers in seconds against hours —
but it is a real gap in the arm), and Op 'sparse_attn_indexer' not present in model incidentally confirms Qwen3-235B has no indexer, unlike GLM-5.2.
docs/34's sharded_state is the fix for this model too, not just bf16 —
the flat param_data.copy_() path never touches weight_loader. That makes the
one-time conversion the unblocking step for the best fast-path candidate we have.
t70-w8a8 baseline reproduced this doc exactly (843 prefill / 6.8 decode),
which validates the harness. Both fusion arms died during load:
pass_manager.py:158
self.passes += [AllReduceFusionPass(config)]
^^^^^^^^^^^^^^^^^^^
NameError: name 'AllReduceFusionPass' is not defined
is_enabled() is falsy on gfx90a (see the root cause in docs/35), so the gate
takes its else branch — which references a symbol imported only under
if current_platform.is_cuda(). ROCm is is_cuda_alike() but not
is_cuda(), so on any ROCm host with fuse_allreduce_rms=True and AITER
unavailable, vLLM crashes instead of degrading. That is an architecture-neutral
upstream bug worth reporting on its own.
Also visible in the resolved config: fuse_norm_quant already defaults to
True, so one of the three "unset" fusion flags was running in every arm ever
measured. Only fuse_allreduce_rms and fuse_act_padding were off. And
disable_custom_all_reduce=True appears because there is no XGMI — so even with
the pass loaded, initialize_aiter_allreduce() may have nothing to bind to.
The most thoroughly characterised model here. Prefill is @15k, decode @25.8k.
| Config | Engine | Weights live | TTFT | Prefill | Decode |
|---|---|---|---|---|---|
| AWQ-Int4, prefetch 50% | vLLM | 42 GB/rank host | 19.0 s | 800.7 | 0.62 |
| AWQ-Int4, prefetch 67% | vLLM | 56 GB/rank host | 20.6 s | 729.9 | 0.47 |
| AWQ-Int4, prefetch 75% | vLLM | 63 GB/rank host | 21.9 s | 695.9 | 0.42 |
| AWQ-Int4, prefetch 75%, no pinning | vLLM | ″ | 23.2 s | 652.5 | 0.40 |
| AWQ-Int4, UVA experts 45 GB | vLLM | host, zero-copy | 184.2 s | 82.4 | 2.40 |
| UD-IQ2_M GGUF | llama.cpp | 135 GB GPU | 69.5 s | 217.8 | 10.85 |
IQ3_XS + FORCE_MMQ |
llama.cpp | 135.6 GB GPU | — | 214.3 | 8.64 |
| UD-Q2_K_XL GGUF | llama.cpp | ~135 GB GPU | 72.8 s | 208.5 | 11.52 |
| IQ3_XS GGUF | llama.cpp | 135.6 GB GPU | 77.5 s | 204.5 | 8.62 |
IQ3_XS, --n-cpu-moe 60 |
llama.cpp | 60 layers on CPU | 94.2 s | 159.7 | 7.47 |
| AWQ-Int4, UVA non-selective | vLLM | host, zero-copy | ✗ 35+ min/request | — | — |
The split is extreme and it is a genuine choice, not a ranking. Best prefill (800.7) and best decode (11.52) come from different engines, different quantizations, and opposite placement strategies — and they differ by 3.9× on prefill and 18× on decode in opposite directions.
Long prompt, short answer → vLLM AWQ-Int4 with prefetch offload. 19 s to first token on 15k, against 70–77 s for any GGUF.
Anything that generates → llama.cpp UD-Q2_K_XL. 11.52 t/s is slow but usable; 0.62 t/s is not.
Prefetch and UVA invert against each other. Prefetch wins prefill by 8.4×;
UVA wins decode by 5.7× (2.40 vs 0.42). Prefetch stages whole layers, so batch-1
decode pays to move weights it barely touches, while UVA's zero-copy reads only
what is actually referenced. An earlier version of this doc called UVA simply
"architecturally worse" — it is worse at prefill and better at decode, and the
original catastrophe was caused by offloading non-selectively (attention
weights included), which --cpu-offload-params experts fixes.
Tried, and it does not merely lose — it damages the host. HSA_XNACK=1 plus
GGML_CUDA_ENABLE_UNIFIED_MEMORY=1 with -ngl 999 on GLM-4.6 IQ3_XS never
finished loading:
llama-server: hsa-runtime/core/runtime/runtime.cpp:2026:
Runtime::VMFaultHandler: Assertion `false && "GPU memory access fault."'
The kernel log shows this is not a missing-page fault: MAPPING_ERROR: 0x0
and WALKER_ERROR: 0x0 (the page is mapped), with PERMISSION_FAULTS: 0x5 and
RW: 0x1 — a device write denied on a resident managed page, which the
driver then failed to promote, degrading a recoverable retry page fault into a
fatal no-retry one. Retry faults are enabled, so amdgpu.noretry is not the
problem. Full analysis in docs/29.
The abort left an rwsem with no live owner, and amdgpu's SVM eviction workers stacked up behind it:
INFO: task kworker/44:10 blocked for more than 122 seconds.
Workqueue: events svm_range_evict_svm_bo_worker [amdgpu]
... blocked on an rw-semaphore, but the owner is not found.
Load average hit 70 and was still climbing. Killing the arm drained it
(70 → 34 → 20) and both GPUs recovered without a reset — rocminfo enumerated
normally at 42–47 °C idle — but nothing stopped on its own: the harness
recorded the failure and advanced to the next -ub value, re-triggering it.
The prefill-amortisation argument for RAM-resident weights with GPU compute is
untouched by this — the fault happens at load, before any of it is
exercised. The idea is not refuted; this route to it is. --offload-backend prefetch on vLLM pursues the same goal with explicit staged copies and no
XNACK. Kernel 7.0.0-28-generic, ROCm 7.14.
benchmarks/matrix/round15_unified_mem.sh now refuses to run without
ALLOW_UVM_HANG=1.
UD-IQ2_M wins both axes — +11.2% prefill and +27.5% decode on a matched 15k/25.8k pair. An earlier partial run put the prefill gain at +6.1% (208 t/s); the completed arm measures 217.8, and the decode gap is the larger effect.
Matched arms, same model (IQ3_XS), same prompts, only the placement changing:
| Placement | Prefill @15k | Prefill @25.8k | Decode @25.8k |
|---|---|---|---|
| auto-fit (135.57 GB on GPU) | 195.9 | 180.9 | 8.51 |
--n-cpu-moe 60 |
159.7 | 151.9 | 7.47 |
--n-cpu-moe 70 |
153.7 | 145.8 | 7.06 |
--n-cpu-moe 80 |
140.9 | 140.4 | 6.77 |
--n-cpu-moe 92 (all experts) |
141.0 | 134.1 | 6.36 |
Monotone: every expert layer moved to the CPU costs throughput, and there is no knee to tune toward. Auto-fit beats the best manual value by 23% prefill and 14% decode.
An earlier note here claimed the curve went "flat past 60". It does not — that
came from comparing an auto-fit cold16k number against --n-cpu-moe
longctx numbers. Matched workloads show a steady decline on both.
Reach for --n-cpu-moe only when the model does not otherwise fit. It is a
capacity mechanism, not a performance one.
Worth stating because the traffic model predicts the opposite and is wrong.
The argument for raising -ub on the CPU path: with E experts and k active
per token, distinct experts touched per chunk is E·(1−(1−k/E)^c), which
saturates at E once c ≳ E/k. GLM-4.6 routes 8 of 160, so saturation is ~20
tokens and -ub 2048 is 100× past it — every chunk drags the whole CPU-resident
expert set across DDR4. Total traffic is then (n/c)·E·bytes, monotonically
decreasing in c, predicting ~2× from 2048 → 4096.
Measured, at --n-cpu-moe 60:
-ub |
Prefill @15k |
|---|---|
| 1024 | 119.6 |
| 2048 | 164.6 |
| 4096 | 158.1 |
4096 is 4% worse, not 2× better. The traffic model is refuted — the expert weights are not what the chunk size is trading against.
Two independent lines say the binding constraint is dequantization work, not bytes moved:
- K-quants beat I-quants by 36% decode on this path at essentially equal byte counts (~135 GB either way, above). Traffic cannot explain a 36% gap between two files of the same size; per-weight instruction cost can — K-quants are scaled integer blocks AVX2 handles directly, I-quants need a codebook lookup with no vector equivalent on Zen3.
/proc/cpuinfohas noavx_vnni, noavx512_vnni, noamx_int8, andlibggml-cpu.sodisassembles to 927vpmaddubsw/ 722vpmaddwd/ zerovpdpbusd— the classic three-instruction AVX2 int8 sequence, which is correct here rather than a missed optimisation, and which fixes the per-weight cost floor.
So the lever on the CPU path is thread placement and quant family, not chunk
size. -t 24 (physical cores) is worth 18% decode over 48; -tb barely matters;
-ub 2048 was already right.
| Weights | Engine | On GPU | Prefill @15k | Prefill @25.8k | Decode |
|---|---|---|---|---|---|
GGUF UD-Q3_K_XL unsloth/...GGUF |
llama.cpp | 104.2 GB | 698.2 | 628.6 | 23.09 |
GPTQ-Int4 ...t235-gptq4 |
vLLM | — | — | — | ✗ never loads |
3.4× the prefill and 2.7× the decode of GLM-4.6 (357B), on a model only 34% smaller. TTFT at 15k is 21.8 s against GLM's 77.5 s.
The reason is that it fits. 104.2 GB sits inside 128 GB of VRAM with room for KV; GLM-4.6's 139 GB does not, and everything after that is paging.
An earlier version of this section compared llama.cpp measurements against a law derived from a vLLM baseline and concluded GLM-4.6 ran "3.5× below prediction", calling the gap the cost of not fitting. That mixed engines. Corrected, using each engine's own 3B-active baseline:
llama.cpp (baseline: Qwen3-30B Q4_K_M, 3,415.7 t/s @ 3B active)
| Model | Active | Predicted | Measured | |
|---|---|---|---|---|
| Qwen3-235B-A22B UD-Q3_K_XL | 22B | ~466 | 698 | 1.5× above |
| GLM-4.6 IQ3_XS | 32B | ~320 | 196–218 | ~1.6× below |
vLLM (baseline: Qwen3-Next-80B W8A8, 7,253 t/s @ 3B active)
| Model | Active | Predicted | Measured | |
|---|---|---|---|---|
| GLM-4.6 AWQ-Int4 + prefetch offload | 32B | ~680 | 695.9 | on the line |
Within llama.cpp the fits/doesn't-fit spread is ~2.2×, not 3.5× — and the 235B beats its own engine's law, which the cross-engine version hid completely.
The law is too crude to carry more than a direction. It ignores quantization format, attention geometry, and batch configuration, and the vLLM row uses a different quant (AWQ-Int4) from its own baseline (W8A8) — where tier 1 measures AWQ-Int4 slower than W8A8 at equal TP, so that row should have over-predicted and did not. Treat these as order-of-magnitude sanity checks.
Choose the largest model that still fits VRAM, not the largest you can page — on llama.cpp with auto-fit placement. The 235B is the sweet spot there and it is measured, not inferred.
Deliberate paging is a different regime. GLM-4.6 AWQ-Int4 under vLLM's prefetch offloader — 75% of expert layers, ~126 GB in host RAM — measures 695.9 t/s prefill (TTFT 21.92 s median, three reps at 15k, VRAM 131.16 GB), against 69.5–77.5 s TTFT for the same model kept on the cards as GGUF.
How much of that is the offloading is not yet established. The arms differ in engine, tensor-parallel layout, batch size and placement simultaneously, and llama.cpp is not generally behind vLLM — at tier 1, GGUF Q4_K_M (3,416) beats vLLM AWQ-Int4 TP=1 (3,002). The isolating experiment is the 67% and 50% offload arms of round 18, which vary only the offload fraction: if prefill barely moves, the win is engine and configuration rather than the offload mechanism. Decode is the opposite story either way — see the offload section.
Same model (GLM-4.6), same placement, ~135 GB on GPU in every case:
| Weights | Family | Prefill @15k | Decode @25.8k |
|---|---|---|---|
| UD-IQ2_M | I-quant | 217.8 | 10.85 |
| UD-Q2_K_XL | K-quant | 208.5 | 11.52 |
| IQ3_XS | I-quant | 195.9 | 8.51 |
No family sweeps both: UD-IQ2_M takes prefill by 4.5%, UD-Q2_K_XL takes decode by 6.2%, and IQ3_XS loses to both despite being the largest.
With experts on the CPU the K-quant advantage widens sharply. At
--n-cpu-moe 60:
| Weights | Prefill | Decode |
|---|---|---|
| UD-Q2_K_XL | 166.2 | 10.13 |
| IQ3_XS | 159.7 | 7.47 |
+36% decode. K-quants are scaled integer blocks that AVX2 handles directly;
I-quants need a codebook lookup per weight, which has no vector equivalent on
Zen3. On the GPU both reach MFMA — ggml_cuda_should_use_mmq admits IQ2_XXS
through IQ4_XS, and every MoE here clears its n_experts > 64 short-circuit
anyway — so the gap only opens once the CPU is doing the arithmetic.
If any expert layer will live on the CPU, pick a K-quant.
| Weights | Why | Evidence |
|---|---|---|
| FP8, any model | CDNA2 has no FP8 ALU. v_mfma_f32_*_fp8_fp8 is rejected by the assembler. Every weight upcasts to bf16, so you pay dequantization and get nothing. |
1,047 t/s vs AWQ-Int4's 3,002 — 2.9× slower |
| any MoE that falls back to WNA16 — including AWQ-Int4 at scale | moe_wna16_weight_loader runs per expert per weight, single-threaded Python. Not a gptq property: AWQ lands here too once Marlin declines the expert layers, and Marlin is CUDA-only on this box. |
gptq: >46 min, never served. AWQ 235B measured 1,339 s/shard × 25 = ~9.3 h, both arms timed out |
compressed-tensors 4-bit with type:"int" |
The one ROCm-capable mixed-precision kernel accepts uint4b8/uint4, not signed int4. AWQ and GPTQ emit the unsigned form and work. |
Fails at load: Quant type int4 not supported |
| W4A8 (any) | Two independent blockers: no ROCm kernel takes int8 activations, and none accepts signed int4 weights. | docs/27 — kernel-by-kernel refusal |
| bf16 for a MoE you restart | 12,366 s load against 60 s for the same model as W8A8. | 206× |
--max-model-len > 131072 on stock vLLM |
Graph capture bakes the Triton fallback into every request. | 10× decode. Fixed by configs/extend_rocm_pa_256k_gfx9.py — 0.75 → 16.0 t/s |
| Speculative decoding | Decode here sits 6.4× off its bandwidth bound, so verifying N+1 tokens costs ~N+1×, not ~1×. Loses even at 100% acceptance. | 6.85 → 6.01 t/s with every token accepted |
Four fields, in config.json and the tensor index. None is in the repo name.
| Check | Want | Costs you if wrong |
|---|---|---|
quantization_config.quant_method |
compressed-tensors |
hours of load time |
...group_0.input_activations |
populated, type: "int" |
"float" is FP8 — dead on CDNA2 |
head_dim |
128 or 192 | no AITER ASM prefill (+8–33%) — bf16 objects only, no fp16 |
num_attention_heads / num_key_value_heads |
exactly 8 or 16 | no ASM paged attention — that is the entire pa coverage (docs/35) |
kv_lora_rank |
check before the head counts | if present the model is MLA and num_key_value_heads is vestigial — misreading this produced a 44× KV error (docs/35) |
tensor index for mtp/nextn |
present, if you want MTP | quantizers strip it silently |
Worked example of why the name lies: cyankiwi/...AWQ-8bit is not AWQ — it
is compressed-tensors pack-quantized with input_activations: null, i.e.
weight-only W8A16. It is a perfectly good checkpoint; it is just not what it
says.
-e HSA_NO_SCRATCH_RECLAIM=1 # required on MI200
-e NCCL_P2P_DISABLE=1 # PCIe-only, no XGMI
-e VLLM_ROCM_USE_AITER=1
-e VLLM_ROCM_USE_AITER_MHA=1
-e VLLM_PREFER_AITER_FA=1 # REQUIRED to select AITER FA — see the retraction above.
--max-model-len 131072 # never higher on stock vLLM
--max-num-batched-tokens 8192 # +11% prefill; default is 2048, plateaus at 8192
--no-enable-prefix-caching # benchmarking only; leave ON in productionllama.cpp: -ub 2048 (4096 and 8192 are both measurably worse), and if you set
-ngl you must also set --n-cpu-moe — common_fit_params() is all-or-nothing.
ggml_cuda_should_use_mmq() returns false past ne11 > 256 for Q4_K/Q5_K, so at
-ub 2048 the attention projections and dense layers dequantize to fp16 and call
hipBLAS instead of using the int8 MFMA kernels. Forcing MMQ on (a compile-time
#ifdef, so it needs a separate build — configs/Dockerfile.llama-forcemmq)
gives opposite answers on the two ends of the matrix:
| Model | Prefill @15k | Prefill @25.8k | Decode |
|---|---|---|---|
| Qwen3-30B Q4_K_M, baseline | 3,415.7 | 2,891.3 | 84.87 |
| Qwen3-30B Q4_K_M, forced | 3,349.8 | 2,866.4 | 85.04 |
| −1.9% | −0.9% | +0.2% | |
| GLM-4.6 IQ3_XS, baseline | 204.5 | 180.6 | 8.62 |
| GLM-4.6 IQ3_XS, forced | 214.3 | 189.5 | 8.64 |
| +4.8% | +4.9% | +0.2% |
Decode is unmoved in both cases (+0.2%, noise), which is what the heuristic
predicts — it keys on ne11, and decode never leaves the small-batch branch
where MMQ already wins.
The prefill split is the interesting part, and it is probably not about the arithmetic. The 30B is fully resident, so hipBLAS's fp16 GEMM is simply fast and MMQ's int8 path is marginally behind it — upstream's cutoff is correct there. GLM-4.6 saturates VRAM, and the dequantize path has to materialise an fp16 copy of the weights to hand to hipBLAS. MMQ consumes the quantized weights directly. When bandwidth and VRAM are the binding constraint, skipping that temporary is worth ~5%; when they are not, it costs 1–2%.
Rebuild with
-DGGML_CUDA_FORCE_MMQ=ONonly for models that saturate VRAM. For anything that fits comfortably, upstream's default is the better choice.
Verify AITER actually ran, don't assume: the .co load line is the proof,
not the backend-selection line.
Overriding with ROCM_AITER_FA <- selected
fwd_hd128_bf16_causal_rtna_group.co <- ACTUALLY LOADED