Skip to content

[AMD] Enable EAGLE speculative decoding for Qwen3.5 FP8 and MXFP4 models with aiter's unified attention - #23146

Merged
HaiShaw merged 6 commits into
sgl-project:mainfrom
hubertlu-tw:aiter_qwen35_mtp_fix
May 5, 2026
Merged

HaiShaw merged 6 commits into
sgl-project:mainfrom
hubertlu-tw:aiter_qwen35_mtp_fix

Conversation

@hubertlu-tw

@hubertlu-tw hubertlu-tw commented Apr 18, 2026 •

Copy link
Copy Markdown
Collaborator

Co-author: @kkHuang-amd, @sogalin

Motivation

Enable EAGLE speculative decoding on top of the aiter unified-attention decode path for Qwen3.5-397B-A17B FP8 and MXFP4 on AMD (gfx950 / MI355X), and fix loading for the Quark-MXFP4 EAGLE/MTP variant.

Before this change:

  • SGLANG_USE_AITER_UNIFIED_ATTN=1 plus --speculative-algorithm EAGLE does not have a unified-attention target-verify path for the non-MLA, topk == 1 EAGLE case.
  • The spec_info decode path can provide token-level ragged indices, while unified_attention expects a 2-D block-level page table.
  • Qwen3.5-397B-A17B-MXFP4 fails to load with EAGLE because the Quark-exported MTP module is bf16 but the loader tries to allocate MXFP4-packed shapes ([Bug] [rocm] qwen 3.5 mtp fp4 broken #23113).
  • --quantization quark is rejected by the CLI even though QuarkConfig is registered.

This PR keeps the draft-decode seqused_k fix and radix-cache behavior out of scope. The draft-decode fix is handled separately in #23461, and radix-cache enablement remains in the separate radix-cache/MTP conflict path. Validation here uses --disable-radix-cache.

Modifications

  • python/sglang/srt/layers/attention/aiter_backend.py
    • When SGLANG_USE_AITER_UNIFIED_ATTN=1, the model is non-MLA, and speculative_eagle_topk == 1, route EAGLE target_verify through unified_attention. The linear draft chain's verify mask reduces to pure causal, so the previous extend_attention_fwd + custom-mask path is not required for this case. This is gated by SGLANG_AITER_UNIFIED_VERIFY (default on for this combination).
    • Build the 2-D block-level page table required by unified_attention for decode-with-spec and target_verify paths without allocating an intermediate bs x max_context_len int32 scratch.
    • Use layer.v_head_dim for the target-verify unified_attention output view so it matches the value cache shape and the legacy extend path.
    • For Qwen3.5 tensor-parallel layouts where one local KV head is replicated across multiple local query heads (for example MXFP4 TP=4), pass stride-0 expanded KV views to unified_attention in target_verify so the unified path preserves the model's local replicated-KV mapping instead of corrupting speculative verification.
    • Keep the unified-attention page-table transform behind self.use_triton_unified_attention and not self.use_mla. MLA and non-unified-attention paths continue using the original token-level spec_info indices.
    • Respect sliding-window attention in target_verify by deriving window_size from layer.sliding_window_size and using the SWA page table when available.
  • python/sglang/srt/layers/attention/triton_ops/aiter_unified_attention.py
    • Add the Triton scatter kernels used by the aiter unified-attention page-table builders.
  • python/sglang/srt/models/qwen3_5_mtp.py
    • Quark-quantized Qwen3.5 MXFP4 checkpoints ship the MTP module in bf16; every mtp.* layer is listed under quantization_config.exclude. Detect that and skip quantization for the MTP module so linear/MoE weight loaders allocate bf16 shapes. Tracks [Bug] [rocm] qwen 3.5 mtp fp4 broken #23113.
  • python/sglang/srt/server_args.py
    • Add "quark" to QUANTIZATION_CHOICES so --quantization quark is accepted (QuarkConfig is already registered in QUANTIZATION_METHODS).

Environment variable:

  • SGLANG_AITER_UNIFIED_VERIFY (default 1 for non-MLA + topk == 1; set to 0 to disable target_verify unified attention).

Accuracy Tests

Latest FP8 validation after review-comment fixes:

Model Run Accuracy Invalid Latency (s) Output throughput (token/s) Gate
Qwen3.5-397B-A17B-FP8 + EAGLE 1 0.949 0.011 62.754 3436.185 Pass
Qwen3.5-397B-A17B-FP8 + EAGLE 2 0.951 0.008 84.644 2553.427 Pass
Qwen3.5-397B-A17B-FP8 + EAGLE 3 0.951 0.009 59.399 3653.503 Pass
Qwen3.5-397B-A17B-FP8 + EAGLE 4 0.945 0.008 58.442 3686.672 Pass

Latest TP=4 validation after fixing replicated-KV target_verify handling:

Model TP Run Accuracy Invalid Latency (s) Output throughput (token/s) Gate
Qwen3.5-397B-A17B-MXFP4 + EAGLE 4 Before fix 0.472 / 0.024 0.236 / 0.581 100.916 / 93.650 4447.324 / 7124.179 Fail
Qwen3.5-397B-A17B-MXFP4 + EAGLE 4 Legacy-verify A/B 0.925 0.006 78.280 2861.588 Pass
Qwen3.5-397B-A17B-MXFP4 + EAGLE 4 Unified-verify fixed run 1 0.920 0.008 78.024 2902.959 Pass
Qwen3.5-397B-A17B-MXFP4 + EAGLE 4 Unified-verify fixed run 2 0.923 0.011 81.640 2700.822 Pass
Qwen3.5-397B-A17B-MXFP4 + EAGLE 4 Unified-verify fixed run 3 0.933 0.006 76.664 2901.184 Pass
Qwen3.5-397B-A17B-MXFP4 + EAGLE 4 Unified-verify fixed run 4 0.933 0.009 75.362 2942.084 Pass
Qwen3.5-397B-A17B-MXFP4 + EAGLE 4 Unified-verify fixed run 5 0.927 0.007 74.698 2981.545 Pass
Qwen3.5-397B-A17B-FP8 + EAGLE 4 Unified-verify fixed run 1 0.947 0.012 74.310 2887.723 Pass
Qwen3.5-397B-A17B-FP8 + EAGLE 4 Unified-verify fixed run 2 0.948 0.011 73.890 2922.611 Pass
Qwen3.5-397B-A17B-FP8 + EAGLE 4 Unified-verify fixed run 3 0.951 0.008 74.399 2916.144 Pass

Earlier full GSM8K validation for the original FP8/MXFP4 enablement:

Model Run 1 Run 2 Run 3 Avg Gate
Qwen3.5-397B-A17B-FP8 + EAGLE 0.946 0.955 0.947 0.949 Pass
Qwen3.5-397B-A17B-MXFP4 + EAGLE 0.929 0.938 0.933 0.933 Pass
Server launch: FP8 + EAGLE
SGLANG_USE_AITER_UNIFIED_ATTN=1 SGLANG_USE_AITER=1 \
SGLANG_AITER_UNIFIED_VERIFY=1 \
python3 -m sglang.launch_server \
  --model-path /data2/Qwen/Qwen3.5-397B-A17B-FP8 --tp 8 \
  --attention-backend aiter --trust-remote-code \
  --model-loader-extra-config '{"enable_multithread_load": true}' \
  --watchdog-timeout 1200 --mem-fraction-static 0.9 \
  --host 0.0.0.0 --port 9000 --disable-radix-cache \
  --enable-aiter-allreduce-fusion --max-running-requests 128 \
  --page-size 16 \
  --speculative-algorithm EAGLE --speculative-num-steps 3 \
  --speculative-eagle-topk 1 --speculative-num-draft-tokens 4
Server launch: MXFP4 + EAGLE
SGLANG_USE_AITER_UNIFIED_ATTN=1 SGLANG_USE_AITER=1 \
SGLANG_AITER_UNIFIED_VERIFY=1 \
python3 -m sglang.launch_server \
  --model-path /data/Qwen3.5-397B-A17B-MXFP4 --tp 8 \
  --quantization quark \
  --attention-backend aiter --trust-remote-code \
  --model-loader-extra-config '{"enable_multithread_load": true}' \
  --watchdog-timeout 1200 --mem-fraction-static 0.85 \
  --host 0.0.0.0 --port 9000 --disable-radix-cache \
  --enable-aiter-allreduce-fusion --max-running-requests 128 \
  --page-size 16 \
  --speculative-algorithm EAGLE --speculative-num-steps 3 \
  --speculative-eagle-topk 1 --speculative-num-draft-tokens 4
Server launch: FP8 TP=4 + EAGLE
SGLANG_USE_AITER_UNIFIED_ATTN=1 SGLANG_USE_AITER=1 \
SGLANG_AITER_UNIFIED_VERIFY=1 \
python3 -m sglang.launch_server \
  --model-path /data2/Qwen/Qwen3.5-397B-A17B-FP8 --tp 4 \
  --attention-backend aiter --trust-remote-code \
  --model-loader-extra-config '{"enable_multithread_load": true}' \
  --watchdog-timeout 1200 --mem-fraction-static 0.9 \
  --host 0.0.0.0 --port 9000 --disable-radix-cache \
  --enable-aiter-allreduce-fusion --max-running-requests 128 \
  --page-size 16 \
  --speculative-algorithm EAGLE --speculative-num-steps 3 \
  --speculative-eagle-topk 1 --speculative-num-draft-tokens 4
Server launch: MXFP4 TP=4 + EAGLE
SGLANG_USE_AITER_UNIFIED_ATTN=1 SGLANG_USE_AITER=1 \
SGLANG_AITER_UNIFIED_VERIFY=1 \
python3 -m sglang.launch_server \
  --model-path /data2/amd/Qwen3.5-397B-A17B-MXFP4/ --tp 4 \
  --quantization quark \
  --attention-backend aiter --trust-remote-code \
  --model-loader-extra-config '{"enable_multithread_load": true}' \
  --watchdog-timeout 1200 --mem-fraction-static 0.85 \
  --host 0.0.0.0 --port 9000 --disable-radix-cache \
  --enable-aiter-allreduce-fusion --max-running-requests 128 \
  --page-size 16 \
  --speculative-algorithm EAGLE --speculative-num-steps 3 \
  --speculative-eagle-topk 1 --speculative-num-draft-tokens 4
GSM8K command used for latest FP8 validation
for i in 1 2 3 4; do
  python3 benchmark/gsm8k/bench_sglang.py \
    --num-questions 1319 --parallel 1319 --num-shots 5 --port 9000
done

Benchmarking and Profiling

sa_benchmark.sh with input_tokens=8192 output_tokens=8192 concurrency=16 num_prompts=64, identical serving flags except for the speculative-decoding flags. Improvement columns use:

  • Lower-is-better: (baseline - candidate) / baseline * 100
  • Higher-is-better: (candidate - baseline) / baseline * 100

Qwen3.5-397B-A17B-FP8: EAGLE on/off

Metric No EAGLE baseline EAGLE Improvement
Benchmark duration (s) 367.03 236.24 -35.6%
Output throughput (tok/s) 1428.44 2219.30 +55.4%
Total token throughput (tok/s) 2856.88 4438.60 +55.4%
Median TPOT (ms) 11.04 5.87 -46.8%
Median TTFT (ms) 1318.99 201.54 -84.7%
Median E2E latency (ms) 91842.25 48291.89 -47.4%
Serving benchmark command
python3 benchmark_serving.py \
  --model /data/Qwen3.5-397B-A17B-FP8 \
  --base-url http://0.0.0.0:9000 --backend sglang \
  --dataset-name random \
  --random-input-len 8192 --random-output-len 8192 --random-range-ratio 1.0 \
  --num-prompts 64 --max-concurrency 16 --request-rate inf --ignore-eos \
  --num-warmups 32 --percentile-metrics ttft,tpot,itl,e2el

Checklist

Review Process

  1. Ping Merge Oncalls to start the process. See the PR Merge Process.
  2. Get approvals from CODEOWNERS and other reviewers.
  3. Trigger CI tests with comments or contact authorized users to do so.
    • Common commands include /tag-and-rerun-ci, /tag-run-ci-label, /rerun-failed-ci.
  4. After green CI and required approvals, ask Merge Oncalls or people with Write permission to merge the PR.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several optimizations and bug fixes for the Aiter attention backend and Quark quantization support. Key changes include the implementation of a unified attention path for EAGLE target verification when topk is 1, which avoids legacy Triton kernels and uses new scatter kernels to build page tables. It also adds a specialized Triton kernel for get_last_loc on HIP to bypass a compiler bug related to mixed-width stores. Additionally, the PR adds support for Quark quantization and handles specific layer exclusions for Qwen3.5 MTP modules. Review feedback identifies a potential dimension mismatch in the unified attention output view and suggests respecting sliding window configurations in the verification path.

v=v_cache.view(
-1, self.page_size, layer.tp_v_head_num, layer.v_head_dim
),
out=o.view(-1, layer.tp_q_head_num, layer.qk_head_dim),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The out tensor view should use layer.v_head_dim instead of layer.qk_head_dim to be consistent with the v cache shape and the legacy extend_attention_fwd path, especially since the code explicitly handles cases where qk_head_dim != v_head_dim (e.g., in lines 2614-2619).

Suggested change
out=o.view(-1, layer.tp_q_head_num, layer.qk_head_dim),
out=o.view(-1, layer.tp_q_head_num, layer.v_head_dim),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hubertlu-tw this?

max_seqlen_k=max_kv_len,
softmax_scale=layer.scaling,
causal=True,
window_size=(-1, -1),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The window_size is hardcoded to (-1, -1), which disables sliding window attention for the target_verify path. While Qwen3.5 might not use it, this backend is shared. It should respect layer.sliding_window_size if present, similar to the forward_decode implementation.

Suggested change
window_size=(-1, -1),
window_size=(layer.sliding_window_size, -1) if layer.sliding_window_size is not None and layer.sliding_window_size > -1 else (-1, -1),

@hubertlu-tw hubertlu-tw mentioned this pull request Apr 18, 2026
5 tasks done

@HaiShaw HaiShaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To address radix cache enabled in separate PR?

@hubertlu-tw hubertlu-tw changed the title [AMD] Enable MTP for Qwen3.5 FP8 and MXFP4 models with aiter's unified attention [AMD] Enable EAGLE speculative decoding for Qwen3.5 FP8 and MXFP4 models with aiter's unified attention Apr 20, 2026
Comment thread python/sglang/srt/layers/attention/aiter_backend.py Outdated
Comment on lines +1009 to +1022
kv_indptr, kv_indices = spec_info.kv_indptr, spec_info.kv_indices
bs = kv_indptr.shape[0] - 1

if self.use_sliding_window_kv_pool:
swa_page_table = (
self.token_to_kv_pool.translate_loc_from_full_to_swa(
kv_indices
)
)

kv_indices = self._transform_table_1_to_real(kv_indices)
swa_page_table = self._transform_table_1_to_real(swa_page_table)
else:
kv_indices = self._transform_table_1_to_real(kv_indices)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When use_mla or use_triton_unified_attention == False will enter this else block.

But the sliding window and _transform_table_1_to_real logic is for unified_attention to use it will broken for use_mla or use_triton_unified_attention == False

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hubertlu-tw this?

@HaiShaw HaiShaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hubertlu-tw Let's break this to 3 separate ones to expedite review:

  1. PR HSA memory-access faults from common.py fix
  2. Leave #23461 fix to itself
  3. Use this PR to address remaining/original issues.

Comment thread python/sglang/srt/layers/attention/aiter_backend.py Outdated
v=v_cache.view(
-1, self.page_size, layer.tp_v_head_num, layer.v_head_dim
),
out=o.view(-1, layer.tp_q_head_num, layer.qk_head_dim),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hubertlu-tw this?

Comment on lines +1009 to +1022
kv_indptr, kv_indices = spec_info.kv_indptr, spec_info.kv_indices
bs = kv_indptr.shape[0] - 1

if self.use_sliding_window_kv_pool:
swa_page_table = (
self.token_to_kv_pool.translate_loc_from_full_to_swa(
kv_indices
)
)

kv_indices = self._transform_table_1_to_real(kv_indices)
swa_page_table = self._transform_table_1_to_real(swa_page_table)
else:
kv_indices = self._transform_table_1_to_real(kv_indices)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hubertlu-tw this?

hubertlu-tw and others added 3 commits April 27, 2026 20:05
aiter_backend.py: when SGLANG_USE_AITER_UNIFIED_ATTN=1 with a non-MLA
model and speculative_eagle_topk == 1, route target_verify through
unified_attention instead of the legacy triton extend_attention_fwd.
Precompute the decode qo_indptr arange once. Two small Triton kernels
build a 2D block page_table for the spec/decode and target_verify
paths. MLA and non-unified-attn paths keep their original code path
unchanged. Opt-out via SGLANG_AITER_UNIFIED_VERIFY=0.

mem_cache/common.py: on HIP + aiter backend, route get_last_loc
through a fused int32-result Triton variant. The legacy
get_last_loc_triton kernel emits a mixed-width int32->int64 store
that Triton mis-compiles on HIP under EAGLE + page_size>1 + aiter
unified attention, producing out-of-range last_loc values. Other
hardware and other attention backends keep the original dispatcher.

qwen3_5_mtp.py: Quark-quantized Qwen3.5 MXFP4 checkpoints ship the
MTP module in bf16 (every mtp.* layer is listed under the exclude
list). Detect that and skip quantization for the MTP module so
linear/MoE weight loaders allocate bf16 shapes. Tracks
sgl-project#23113.

server_args.py: add "quark" to QUANTIZATION_CHOICES so the CLI
accepts --quantization quark (the QuarkConfig scheme is already
registered under QUANTIZATION_METHODS).
Move the aiter unified-attention scatter kernels into triton_ops and tighten the target-verify metadata/output handling so the PR keeps only the intended EAGLE enablement changes.

Made-with: Cursor
@hubertlu-tw
hubertlu-tw force-pushed the aiter_qwen35_mtp_fix branch from 8f6e295 to 280d254 Compare April 27, 2026 21:05
@hubertlu-tw

Copy link
Copy Markdown
Collaborator Author

To address radix cache enabled in separate PR?

Yes. I will keep radix-cache enablement out of this PR. This PR continues to validate with --disable-radix-cache, and radix-cache behavior should be handled by the separate radix-cache/MTP conflict PR.

@hubertlu-tw

hubertlu-tw commented Apr 27, 2026 •

Copy link
Copy Markdown
Collaborator Author

@hubertlu-tw Let's break this to 3 separate ones to expedite review:

  1. PR HSA memory-access faults from common.py fix
  2. Leave [AMD] Add kimi-k2.5 eagle3 support with unified attention. #23461 fix to itself
  3. Use this PR to address remaining/original issues.

Preserve the local replicated-KV head mapping in the unified target_verify path so Qwen3.5 TP=4 speculative decoding no longer corrupts verification outputs.

Made-with: Cursor
@hubertlu-tw

Copy link
Copy Markdown
Collaborator Author

Hi @HaiShaw @kkHuang-amd,
Could you please review the PR again? I have addressed/refactored all of the above comments.
In addition, I also validated the workloads with the FP8 model (Qwen/Qwen3.5-397B-A17B-FP8) and the MXFP4 model (amd/Qwen3.5-397B-A17B-MXFP4) with both TP=8 and TP=4. Thanks.

sogalin added a commit to sogalin/sglang that referenced this pull request Apr 28, 2026
Layer the kimi-k2.5 EAGLE3 draft-decode seqused_k fix from sgl-project#23461 on top
of the latest sgl-project#23146 ragged-to-paged converter. Under EAGLE draft decode
the forward_decode unified_attention branch was passing forward_batch.seq_lens
as seqused_k. That value is the target prefix and does not include draft
tokens committed in earlier draft steps of the same round, so draft step
i > 0 attended only over the prefix and missed the K/V the draft had just
written, lowering accept length on Kimi-K2.5 from ~3.97 to ~3.26.

Compute the per-row kv length from spec_info.kv_indptr inside
_build_unified_page_table_from_spec (or write it in-place into a static
cuda-graph buffer cuda_graph_spec_seqused_k), stash it on
ForwardMetadata.seqused_k, and route it into the unified_attention call.
The non-spec path still uses forward_batch.seq_lens unchanged.

Verified on Kimi-K2.5 MXFP4 TP8 + EAGLE3 with SGLANG_USE_AITER_UNIFIED_ATTN=1
(random 8k in / 1k out, concurrency 4, 32 prompts):

  before: Accept length 3.26, output 675 tok/s
  after : Accept length 3.97, output 800 tok/s  (+18%% throughput)

@HaiShaw HaiShaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hubertlu-tw Please add link to radix-cache fix issue or later PR.

@HaiShaw
HaiShaw merged commit c2db19f into sgl-project:main May 5, 2026
174 of 207 checks passed
Chronostasys pushed a commit to MindLab-Research/sglang that referenced this pull request Aug 24, 2026
…els with aiter's unified attention (sgl-project#23146)

Co-authored-by: wunhuang <wunhuang@amd.com>
Co-authored-by: sogalin <39478626+sogalin@users.noreply.github.com>
omirosh added a commit to omirosh/sglang that referenced this pull request Sep 24, 2026
`_mtp_quant_config` currently drops the quantization config for the whole
MTP module as soon as any `mtp.*` layer appears in a Quark exclude list.

That was correct for the checkpoints sgl-project#23146 targeted, where the MTP module
is entirely bf16 and the exclude list covers all of it. It is wrong for
mixed checkpoints, which keep the 512 routed experts in MXFP4 and exclude
only attention, the shared expert and fc. There the whole-module skip makes
the MoE weight loaders allocate bf16 experts, and the MXFP4 checkpoint
shards no longer fit them.

Use the routed experts as the signal instead: they are the bulk of the
draft, so skip only when they are excluded as well. Fully-bf16 MTP
checkpoints still take the skip, because a list that enumerates literal
`mtp.*` names for a bf16 module also enumerates its expert layers. For
mixed checkpoints the config is kept and the ordinary per-layer exclusions
keep attention, the shared expert and fc in bf16.

Reproduced on amd/Qwen3.8-2.4T-A95B-Quark-MXFP4 (NEXTN, TP8, MI350X).

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

amd run-ci CI: run the baseline test suite on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants