Nvidia is estimated to generate 74% of inference tokens, while AMD is less than 20%. This gap is surprising, looking at the hardware specs both offer:
| Specification | AMD MI300X | NVIDIA H100 SXM |
|---|---|---|
| HBM memory | 192 GB HBM3 | 80 GB HBM3 |
| Memory bandwidth | 5.3 TB/s | 3.35 TB/s |
| Dense FP8 compute | 2.61 PFLOP/s | 1.98 PFLOP/s |
| Sparse FP8 compute | 5.22 PFLOP/s | 3.96 PFLOP/s |
| Approx. monthly rental | $1,891 | $3,219 |
At Hopper, we are big users of H100s for voice inference. So we started optimizing inference on an AMD MI300X to see how good we could get it. Let's first setup the system and the goals we are targeting.
- Model: Qwen3.6-35B-A3B-FP8
- GPU: AMD MI300X
- TP1, no batching : we optimize single cold prompt performance
- Achieve highest possible TTFT and throughput across 1–8K prompts, maintaining quality (>97% in τ²-bench)
Baseline
We started with a plain vLLM serving containing full hip-graph (think cuda graphs for AMD) capture with a ROCM_ATTN attention backend. Here is the launch command:
vllm serve "$MODEL_DIR" \
--served-model-name qwen-3.6-a3b \
--tensor-parallel-size 1 \
--max-model-len 262144 \
--max-num-seqs 1 \
--max-num-batched-tokens 12288 \
--gpu-memory-utilization 0.9 \
--enable-chunked-prefill \
--default-chat-template-kwargs '{"enable_thinking": false}' \
--speculative-config null \
--attention-backend auto \
--dtype auto \
--limit-mm-per-prompt '{"image": 0, "video": 0}' \
--compilation-config '{
"cudagraph_capture_sizes": [
1,2,3,4,5,6,7,8,12,16,24,32,40,48,56,64,72,80,88,96,
104,112,120,128,256,512,768,1024,1280,1536,1792,2048,
2304,2560,2816,3072,3328,3584,3840,4096,4352,4608,4864,
5120,5376,5632,5888,6144,6400,6656,6912,7168,7424,7680,
7936,8192
],
"max_cudagraph_capture_size": 8192,
"cudagraph_mode": "FULL_AND_PIECEWISE"
}'
The following graph shows the TTFT sweep on this server covering 91 exact prompt lengths from 1 to 8,192 :
The ridges in the middle are because the input sizes match the graph capture boundaries. This will be important later when we are tuning the kernel selection.
Following are the decode numbers on same server:
| Workload | Decode speed | TPOT | Request TTFT |
|---|---|---|---|
| Varied prompts at 4k | 77.878 tok/s | 12.841 ms | 170.540 ms |
Now, lets see where the baseline spends its GPU time. The table below shows the costliest kernels at 4k prompt length.
| Kernel | Role | Calls | Device work | Share |
|---|---|---|---|---|
| Prefill | ||||
fused_moe_kernel.kd |
Routed experts | 160/request | 40.839 ms/request | 27.7% |
_w8a8_triton_block_scaled_mm.kd |
Dense FP8 | 320/request | 33.807 ms/request | 22.9% |
_fwd_kernel.kd |
Full-attention core | 20/request | 21.923 ms/request | 14.9% |
| Decode | ||||
kernel_paged_attention_2d.kd |
Paged attention | 10/token | 5.157 ms/token | 36.5% |
_w8a8_triton_block_scaled_mm.kd |
Dense FP8 | 160/token | 2.340 ms/token | 16.5% |
fused_moe_kernel.kd |
Routed experts | 80/token | 1.254 ms/token | 8.9% |
per_token_group_quant_8bit_kernel |
FP8 activation quantization | 200/token | 0.952 ms/token | 6.7% |
topkGating<8,256,…> |
MoE routing | 40/token | 0.396 ms/token | 2.8% |
Few observations :
- The largest prefill kernel is the routed MoE at 40.8 ms. This is expected with the model's architecture : Qwen3.6 contains 40 layers with a 256-expert, top-8 MoE in every layer.
- Full attention was still substantial at 22.0 ms, but it did not dominate prefill as it would in an all-attention model because only a quarter of Qwen3.6’s layers use full attention.
- The logs also shows that a 4096 prompt is executed as 2 model forwards, 3168 first and then 928 in another. This is because vLLM selected a page size of 1,056 tokens.
- During decode, the costliest kernel is the paged attention at 5.157 ms because every new token has to query KV cache which starts at 4K tokens and grows throughout generation.
Optimizing kernels for MI300X
The next step is to optimize performance for each individual kernel, by specializing on shapes needed by our model. We adapted AMD's AITER for this. AITER provides optimized Triton, Composable Kernel, and assembly implementations for attention, GEMM, MoE, quantization, and other common inference operators. A kernel that performs well for M=4096 prefill may be poor for M=1 decode. So, we benchmark and find compatible implementations for each (M,N,K) or MoE shape.
The exact-4K tuning invocation looks like:
MODEL=qwen3.6-35b-a3b-fp8 \
bash profile/run.sh dev tune_mi300x_aiter.py \
--component all \
--dense-m-values 4096 \
--dense-shape all \
--dense-libtype both \
--moe-tokens 4096 \
--warmup 5 \
--iters 30 \
--out results/mi300x_aiter_tuning
gfx942 is AMD’s compiler target (think AMD's sm_90) for the CDNA3 architecture used by MI300X. It tells AITER which hardware-specific kernels and tuning configurations are valid for the GPU. AITER maintains tuned dispatch tables where each row effectively says which kernel to choose given a GPU, data_type and (M,N,K) shape.
For example, here is an entry from the block-scaled A8W8 GEMM tuning table used in our MI300X run:
"gfx": "gfx942",
"cu_num": 304,
"M": 2048,
"N": 7168,
"K": 2048,
"libtype": "ck",
"kernelId": 0,
"splitK": 0,
"us": 78.7607
This tells AITER that, on a gfx942 GPU, an A8W8 block-scaled mat mul with shape M=2048, N=7168, and K=2048 should use Composable Kernel configuration 0 without splitting the K dimension and this configuration took 78.76 microseconds in tuning.
During the tuning sweep, a 2,048-token request took 108.95 ms TTFT, while the 2,049-token request fell to 72.64 ms. We found that this was because the 2048-request selected a slower kernel. The dispatch table contained optimized rows for M=1 and M=4096, but not for the M=2048 bucket. The 2,048-token request therefore used fallback kernels, while the next bucket was padded into an already-optimized shape (M=4096). So, this tuning process adds the most perfomant kernels at every shape.
From multiple such passes across 1-8k tokens, the following prefill and decode kernels gave the best results:
| Hot path | Model shape | Baseline path | MI300X-specialized path | Changed result |
|---|---|---|---|---|
| Prefill attention | Exact 4K; 10 attention layers; 16 Q / 2 KV heads; head dim 256 | ROCM_ATTN _fwd_kernel |
CK-Tile FmhaFwdKernel;128×128 tile
|
22.02 → 4.80 ms/request, −78.2% |
| Decode attention | M=1, starting from 4K context |
kernel_paged_attention_2d; 10 calls/token |
paged_attention_ll4mi_QKV_mfma16 plus reduction; 20
shorter calls/token
|
5.157 → 0.256 ms/token, −95.0% |
| Dense FP8 |
Five (N,K) projections; prefill
M=4096, decode M=1
|
Triton _w8a8_triton_block_scaled_mm |
CK XDL block-scaled GEMMs selected from the shape table | Prefill 35.52 → 19.22 ms/request, −45.9%; decode 2.532 → 1.748 ms/token, −31.0% |
| Routed MoE | Hidden 2048, intermediate 512, 256 experts, top-8, block-FP8 | fused_moe_kernel; 160 main prefill calls |
One-stage persistent ASM, block_m=64; 40 calls.
Decode uses two CK MoE stages
|
Prefill 43.03 → 20.49 ms/request, −52.4%; decode 1.650 → 1.133 ms/token, −31.3% |
| KV/cache geometry | Exact-4K prefill | 1,056-token physical page | 4,096-token page and prefill chunk | 3.09 → 1.20 ms/request, −61.1% |
At exact 4K, TTFT fell from 166.666 ms to 95.076 ms (43.0% reduction). This improvement persisted across the longer prompts. The following chart shows this improvement compared to baseline :
With decode attention reduced by 95%, throughput nearly doubled from 77.878 tok/s to 152.891 tok/s. Here are the details for a 4K prompt:
| Configuration | Decode speed | TPOT | Request TTFT |
|---|---|---|---|
| Baseline vLLM | 77.878 tok/s | 12.841 ms | 170.540 ms |
| Using specialized AITER | 152.891 tok/s | 6.541 ms | 110.066 ms |
Speculative decoding: native MTP vs. DFlash
With the individual kernels optimized, to improve throughput further we tested two speculative decoding branches : Qwen's native MTP vs DFlash.
Native MTP reuses the prediction layer already embedded in the Qwen checkpoint. Because this model has one MTP layer, vLLM invokes it sequentially three times to propose three draft tokens.
DFlash instead uses a separate six-layer BF16 draft model to predict fifteen draft positions in parallel.
Speculator models take the tokens generated so far and propose the next K candidate tokens (K=3 for MTP, K=15 for DFlash). The target model scores those candidates in one verification pass, and the system accepts the valid prefix until the first rejection. The fraction of proposed draft tokens that survive is the acceptance rate.
When acceptance rate is high, one verification pass can commit several tokens. On the controlled workload with high acceptance rate >90%, MTP reached 329–331 tok/s, while DFlash reached 1,063 tok/s.
Here is how the three configurations behaved across lower- and high-acceptance workloads:
| Configuration | Decode speed at low acceptance rate | Decode speed at high acceptance rate |
|---|---|---|
| Current best | 152.9 tok/s | 152.9 tok/s |
| Native MTP |
217.9 tok/s 56.3% acceptance |
329–331 tok/s 100.0% acceptance |
| DFlash | 214.8 tok/s 15.3% acceptance |
1,063 tok/s 98.9% acceptance |
On varied MI300X prompts, MTP's three draft positions were accepted at approximately 77.7%, 53.5%, and 36.8%. DFlash began at 71.7%, then fell to 44.5%, 27.7%, 18.2%, 11.7%, and 7.9% over its first six positions. DFlash still committed slightly more text per verification round, but its wider draft and verification work erased that advantage.
The DFlash's wider proposer also costs more to start. At 4K input tokens, TTFT was 95.1 ms for the tuned target, 102.1 ms with MTP K3, and 114.3 ms with DFlash B16. MTP added approximately 7.4% to MI300X's one-token TTFT, while DFlash added 20.2%.
While MTP K3 performs well on general workloads, DFlash would be very beneficial if we could train the speculator model on predictable, domain-specific outputs. We are seeing >800 tok/s with narrow, domain-specific speculators on healthcare and insurance datasets.
Results and comparison with H100
First, here is the full progression on MI300X. The TTFT chart uses the full 1–8K one-token sweep, while the decode table reports median native throughput after the first token at 4K.
MI300X progression
| Configuration | Varied outputs | High-acceptance control |
|---|---|---|
| Baseline vLLM | 77.9 tok/s | 77.9 tok/s |
| Tuned AITER target | 152.9 tok/s | 152.9 tok/s |
| Native MTP K3 | 217.9 tok/s 56.3% acceptance |
329.9 tok/s 100.0% acceptance |
| DFlash B16 | 214.8 tok/s 15.3% acceptance |
1,062.7 tok/s 98.9% acceptance |
H100 comparison
| Configuration | Varied outputs | High-acceptance control |
|---|---|---|
| Tuned CUDA target | 244.3 tok/s | 244.2 tok/s |
| Native MTP K3 |
345.3 tok/s 58.4% acceptance |
514.9 tok/s 100.0% acceptance |
| DFlash B16 | 208.8 tok/s 13.3% acceptance |
1,105.3 tok/s 99.0% acceptance |
MI300X delivered lower TTFT in every matched configuration: 95.1 vs. 99.7 ms without spec-decoding, 102.1 vs. 104.8 ms with native MTP, and 114.3 vs. 124.3 ms with DFlash. That advantage reversed at longer contexts, with H100 achieving roughly 21% lower TTFT at 8K. While H100 remains substantially stronger for general decode and longer prompts, MI300X is highly competitive at shorter prompts (4k) prefill.