Skip to content

feat(moe): consume prepared stage1 activation scales - #4762

Open
JohnQinAMD wants to merge 2 commits into
ROCm:mainfrom
JohnQinAMD:perf/prepared-stage1-activation-scale
Open

feat(moe): consume prepared stage1 activation scales#4762
JohnQinAMD wants to merge 2 commits into
ROCm:mainfrom
JohnQinAMD:perf/prepared-stage1-activation-scale

Conversation

@JohnQinAMD

@JohnQinAMD JohnQinAMD commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Why

fused_moe_2stages always quantizes an A8W4 stage-1 input when the selected
kernel declares prequant=True. That prevents a caller-owned producer from
passing an already-quantized activation and its consumer-ready, route-sorted
E8M0 scale layout: the same activation is quantized and materialized again.

The prepared path must also fail closed. Dtype, contiguity, and scale column
count alone do not prove that a scale tensor has the route-SBM32 preshuffled
layout or enough rows for the selected metadata. Accepting an undersized or
token-major tensor could otherwise cause invalid reads or incorrect output.

What changed

Consume the caller-provided activation directly only when the complete
prepared-stage1 contract agrees:

  • selected kernel metadata requires prequantization;
  • activation is contiguous 2D FP8 and matches q_dtype_a;
  • scale is same-device, contiguous 2D E8M0 with the expected columns;
  • the caller supplies the recognized
    mx_e8m0_route_sbm32_preshuffled_v1 layout tag;
  • scale rows cover both the sorted-ID extent and metadata-required blocks.

Missing/wrong layout tags, undersized or token-major scales, BF16 callers,
non-prequant kernels, and callers without scales retain the existing path.
This PR deliberately contains no model-specific selector or producer.

Tests

python3 -m py_compile \
  aiter/fused_moe.py \
  op_tests/test_fused_moe_prequant.py

ruff check \
  aiter/fused_moe.py \
  op_tests/test_fused_moe_prequant.py

pytest -q op_tests/test_fused_moe_prequant.py
# 15 passed

git diff --check origin/main...HEAD

The negative cases cover missing and wrong tags, undersized and token-major
scales, non-vector sorted IDs, and metadata-driven extents. On gfx950, the
consumer was also exercised with a prepared FP8 activation and sorted E8M0
scales through the production two-stage A8W4 kernels. Output and repeated HIP
Graph replay were bit-exact against the existing path.

This PR makes no standalone performance claim; the producer that removes the
redundant work is intentionally split into a follow-up.

@JohnQinAMD
JohnQinAMD requested review from a team and a lite review from Copilot August 14, 2026 13:27
@JohnQinAMD
JohnQinAMD force-pushed the perf/prepared-stage1-activation-scale branch from ab5cabe to d8c367b Compare August 14, 2026 13:28
@github-actions

Copy link
Copy Markdown
Contributor

🏷️ CI Guide

Runs automatically on every PR:

  • ✅ Pre-checks (submodule verification, code formatting)
  • ✅ Aiter op tests (gfx942 + gfx950)
  • ✅ Triton tests on MI35X (only when aiter/ops/triton/** or related paths are changed)

Extended tests (opt-in via labels):

Label Tests
ci:gfx1250-ffm-triton Run the five-shard gfx1250 FFM Triton test suite
ci:triton-300x Run an additional Triton test job on MI300X in PRs; main branch always runs both MI35X and MI300X
ci:sglang SGLang integration tests: DeepSeek-R1-MXFP4 accuracy, Qwen 3.5 accuracy
ci:atom ATOM benchmark: DeepSeek-R1-0528, GPT-OSS-120B
ci:atom_full ATOM accuracy suite for PR and main models from ATOM models_accuracy.json
ci:vllm vLLM benchmark: GPT-OSS-120B, DeepSeek-R1-0528, Kimi-K2.5
ci:all All standard extended tests (excludes ci:atom_full)

Only add ci:atom_full for FlyDSL or Triton upgrades.
Add labels via the sidebar or gh pr edit 4762 --add-label <label>

Copilot AI 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.

Pull request overview

This PR updates fused_moe_2stages to optionally consume a caller-prepared stage-1 activation (already quantized) and its corresponding sorted E8M0 scale layout, avoiding redundant quantization when the selected kernel requires prequant=True.

Changes:

  • Add _is_prepared_stage1_input() gate to detect when a prepared stage-1 activation/scale pair can be consumed directly.
  • Update fused_moe_2stages to bypass the A8W4 quantize+sort path when the prepared-input gate is satisfied.
  • Add a focused unit test for the prepared-input gating logic.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
aiter/fused_moe.py Adds prepared-input gate and uses it to skip redundant stage-1 quantization for prequant kernels.
op_tests/test_fused_moe_prequant.py Adds unit coverage for the prepared-input gating behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread op_tests/test_fused_moe_prequant.py Outdated
Comment on lines +35 to +37
assert (
_is_prepared_stage1_input(metadata, hidden_states, q_dtype, scale) is expected
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 37fb420. The test matrix now rejects uint8/FP32 scales and a wrong 1x32 column layout, in addition to the existing prequant/dtype/no-scale negative cases. The positive case uses the exact E8M0 dtype.

Comment thread aiter/fused_moe.py Outdated
q_dtype_a: torch.dtype | None,
a1_scale: torch.Tensor | None,
) -> bool:
return metadata.prequant and hidden_states.dtype == q_dtype_a and a1_scale is not None

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 37fb420. The fast path is now limited to contiguous 2D MXFP8 activations plus same-device contiguous 2D E8M0 scales whose second dimension exactly matches the 1x32 activation layout. BF16/FP16 and unrelated scale dtypes/layouts no longer satisfy the gate.

Allow a caller to pass an already-quantized MXFP8 stage-1 activation and its sorted E8M0 scale layout to fused_moe_2stages. This avoids launching fused_dynamic_mxfp8_quant_moe_sort a second time when a producer already owns both outputs.

The fast path is deliberately restricted to contiguous 2D FP8 activations and same-device contiguous E8M0 scales with the expected 1x32 column shape. BF16/FP16 inputs, unrelated scale dtypes or layouts, non-prequant kernels, and callers without scales retain the existing path.

Keep the generic consumer contract separate from the Kimi-K3 exact-M3 producer so each boundary can be reviewed and validated independently.

Signed-off-by: Yanyuan Qin <yanyuan.qin@amd.com>
@JohnQinAMD
JohnQinAMD force-pushed the perf/prepared-stage1-activation-scale branch from d8c367b to 37fb420 Compare August 14, 2026 13:49
The prepared Stage-1 fast path previously inferred a route-sorted, SBM32-preshuffled E8M0 layout from dtype, contiguity, and column count alone. An undersized or token-major scale tensor could therefore enter the consumer and produce invalid reads or incorrect output.

Require the explicit preshuffled layout tag and enough scale rows for both sorted_ids and the metadata block extent. Unsupported layouts fail closed to the existing quantization path. Add negative coverage for missing and wrong tags, undersized and token-major scales, non-vector sorted IDs, and metadata-driven extents.

Validation: 15 unit tests; Ruff; Black; compileall; diff-check.

Signed-off-by: Yanyuan Qin <yanyuan.qin@amd.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants