Repository navigation
fix(npu): keep profile routing decisions inside an opaque op - #500
Conversation
508c90c to
1da04df
Compare
|
Could we fix the compilation boundary in AFD's Attention-side routing instead of replacing the upstream In the pinned stack, native Ascend reads
Please first check whether the profile-dependent expert-ID balancing can live inside an existing AFD opaque custom op; if there is no suitable boundary, consider a minimal routing custom op with the appropriate fake implementation. Keep the Validation should show that profiling still uses balanced expert IDs, subsequent compiled requests use live routing, and compilation remains enabled, with NPU correctness and performance evidence for the affected path. |
|
Thanks, agreed. The fix should restore the compilation boundary in AFD's Attention-side routing rather than replace the upstream profiling lifecycle. I'll first check the existing custom-op boundaries and isolate the profile-dependent expert-ID balancing as narrowly as possible. I'll then remove the runner patch and validate balanced profiling, live routing with compilation enabled, and NPU correctness/performance. |
|
Here is the plan we settled on for this PR. Approach. Rather than replacing the upstream profiling lifecycle, we restore the compilation boundary in AFD's Attention-side routing. The profile-dependent balancing now runs behind an opaque custom op: def afd_balance_routing_ids_(topk_ids: torch.Tensor, num_experts: int, force_balance: bool) -> None:
from vllm_ascend.ascend_forward_context import _EXTRA_CTX
from afd_plugin.routing_balance import _force_balanced_topk_ids
if force_balance or _EXTRA_CTX.in_profile_run:
_force_balanced_topk_ids(topk_ids, num_logical_experts=num_experts)It is registered with What this PR becomes. The Validation so far (CPU only). A regression loads the real op through a fixture-scoped registration ( Still open. Device evidence is not collected yet: a balanced profile run, live routing under graph replay with compilation enabled, and correctness/performance. Our test node currently fails to initialize the MoE all-to-all HCCL group on both this candidate and the pre-change baseline, so this is an environment issue being resolved first; I will post the device results here once it is clear. I will update this PR to the new implementation once the device checks are in. If you would rather see the boundary fix as a fresh PR against current |
|
Device checks are in for the replacement implementation (opaque routing-id boundary, 1. Profile balancing and live routing through the op Standalone NPU check against the registered op, read outside any capture: With 2. Compilation, capture and replay in the served model Capture completed 5/5, matching the five configured capture sizes. The first replay is confirmed by the runtime's message. Per-request replay is not instrumented and I did not add a probe, so I make no claim about it beyond that. 3. Correctness and performance GSM8K (1,319): 90.14 on this configuration. Same-day A/B, identical configuration and device layout, only the checkout differs:
The medians are 0.9% apart and the best new run is 0.2% above the old one. No regression is observed in this round, but the sample is small (n=1 old, n=2 new) and this does not establish equivalence. Earlier sessions measured 326k-329k for this configuration; the same-day old-state run also lands at 312k, which points at an environmental difference rather than the change. Limits: one node, one workload, n=1 old arm and n=2 new arm, and this round holds |
1da04df to
f9377f2
Compare
|
Re-reviewed head [P2] Register the op in the existing V2 gate regression test. The new unconditional call at Remaining NPU validation: the reported device results are from integrated commit |
|
Please fix DCO and pre-commit |
|
Follow-up review of
The device evidence still references integrated commit |
The Attention-side gate reads the profile flag in plain Python, so tracing specializes the branch on the value seen while profiling and the compiled graph keeps applying profile-time balancing to live routing. Move the decision into a mutating custom op that reads the flag at execution time, register it during worker initialization, and leave the gate GEMM and topk inside the compiled region. The pure tensor helper moves to its own module so the compatibility layer no longer imports model code. Targeted tests: 17 passed. Maintenance suite: PASS. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
The gate tests and the operator checkpoint script call the gate without the AFD worker installer, so torch.ops.vllm.afd_balance_routing_ids_ was missing and those entry points raised AttributeError. Register the op explicitly in the checkpoint script after device init, and provide a shared CPU registration fixture that the gate tests use through their conftest. Targeted: 30 passed. Maintenance suite: PASS. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
The V2 gate regression calls the gate directly and needs the same shared CPU registration fixture the V4 tests use. Local collection: the module needs a full vLLM install, so it skips here. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Device check for the routing boundary: compile once through the profile path, then reuse that compiled callable for two distinct non-balanced live inputs, and replay an ACL graph captured with profile mode disabled. Uses the production op registration and the real forward context. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
The profile flag lives on the forward context, so the check builds one with create_forward_context and installs it with override_forward_context instead of assigning to the proxy. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
create_forward_context needs a VllmConfig; ForwardContext plus the in_profile_run attribute set_forward_context installs is enough here. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
The first compilation now happens with the profile flag set, which is the order that exposed the bug, and its output is asserted to be balanced. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Fold the balancing helper into compat/npu/routing_ids.py so the op and its helper live together, drop the package-root helper module, and let the shared CPU registration fixture serve both the op tests and the gate tests. Document the module's ownership and registration lifecycle, and record how to run the device check. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
SPDX headers on the new files, plus the import reformat the hooks applied. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Narrow optional gate types before routing, retain a typed torch import in CPU tests, populate the registration stand-in through its namespace, and mark the documented output as text. Full pre-commit passes with Git Bash available; targeted tests pass 30 cases and the maintained suite passes 243 cases. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
c04dec6 to
f06e43b
Compare
The pinned Ascend exposes in_profile_run via additional_kwargs, so the check now builds that layout and asserts the read before exercising the op. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
_EXTRA_CTX reads additional_kwargs only when VLLM_USE_V2_MODEL_RUNNER is set, so the check now enables that mode before importing vllm. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
The pinned stack exposes in_profile_run through the V2 contextvar, so the check now uses override_mrv2_in_profile_run and asserts the marker reads back. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Writing through _EXTRA_CTX keeps the setter and getter on the same branch, instead of guessing between additional_kwargs and a plain attribute. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Check both that the write reaches additional_kwargs and that the op's read path agrees, so the check cannot pass on the legacy layout. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
sync_v2_extra_kwargs decides where extras live; call it before writing the marker and assert the cache really switched. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
|
LGTM |
Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
vllm-project#500 landed on main and replaced the runner patch with an opaque routing op, so this branch no longer installs or tests the old patch. Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
## Background The synchronous CAMP2P path normally runs attention on the Attention role and the router, shared experts, and routed experts on the FFN role. This PR makes router/shared placement an explicit deployment option. With `compute_gate_on_attention=true`, Attention computes top-k and shared experts, while FFN consumes the external routing and runs the routed experts. ## Motivation **Improve eager execution through a different A/F work split.** The measured Attention-side placement delivers **+17.6% throughput** versus the same-commit FFN-side placement in the evaluated eager workload. **Establish the routing contract for quantized activation transport.** Once FFN receives the routing alongside the activation, it can consume an activation quantized on Attention. The MXFP8 pass-through builds on this contract in #506. The option is **off by default** and belongs in the common `additional_config["afd"]` block. ## Implementation ```text Default Attention : attention FFN : router + shared experts + routed experts compute_gate_on_attention=true Attention : attention + router + shared experts FFN : transported top-k + routed experts ``` - **Reuse the A2E routing slots.** Send expert IDs as `int32` and top-k weights as `float32` through the existing ids channel. - **Owned prepare and comm subclasses.** `AFDPrepareAndFinalizeWithAllGather` and `AFDPrepareAndFinalizeWithMC2` provide `prepare_external`, which takes the routing for the local input rows and returns the complete `MoEPrepareOutput` together with the aligned routing. AFD comm subclasses override only `_get_prepare_finalize()`, so the native selector still decides which path runs and ordinary execution keeps the upstream classes. There are no global prepare patches left. - **Explicit routing contract.** The routing is an argument and a return value; nothing is stashed on the upstream class, and the module no longer reads or clears an `afd_external_routing` attribute. - **One MoE component.** `AFDExternalRoutingMoE` builds its comm candidates during model initialization, where the config context exists, and each forward only looks up the candidate matching the native selection. It runs prepare, expert execution and finalize, switches the forward-context comm for the duration of the call and restores it on every exit path, and rejects unsupported types such as fused-MC2. - **Consume the external top-k on FFN.** The component hands the aligned routing straight to expert execution. - **Place shared experts on Attention.** Run them between activation send and routed-output receive, then combine. - **Keep protocol selection explicit.** The model configuration selects token-ID or external-routing semantics; the model itself only builds the component for the synchronous FFN role and delegates. The supported opt-in configuration is synchronous CAMP2P, DeepSeek-V4, TP=1, without DBO or ubatching. Both roles must use matching routing settings. ## Performance and accuracy Measured at `828582a` on one A5 4A4F node during the same evening session, with BF16 A2E transport and automatic communication selection: | Configuration | Eager throughput (token/s) | Graph throughput (token/s) | | --- | ---: | ---: | | Attention-side gate (`828582a`) | **192,887** | **326,400** | | FFN-side gate (`828582a`) | 163,997 | 331,455.5 | | Clean baseline (`bcc0126`) | 164,543 | 333,407 | | Comparison | Eager | Graph | | --- | ---: | ---: | | Attention-side vs FFN-side placement | **+17.6%** | **-1.5%** | | FFN-side placement vs clean baseline | -0.3% | -0.6% | The result supports Attention-side placement as an eager optimization and a foundation for MXFP8 pass-through. Placement remains opt-in, allowing deployments to select the appropriate A/F split for their execution mode. Graph-mode Attention-side routing scored **90.60% on the full 1,319-question GSM8K evaluation**, with normal generated output. Measurement setup: concurrency **128**, reported input **8,275** / output **32** tokens per request, `max-num-batched-tokens=3072`, `max-num-seqs=128`, and zero failed requests. Graph uses `FULL_DECODE_ONLY` with capture sizes `[8,16,32,64,128]`; eager uses `--enforce-eager`. Each cell has two runs from the same firmware/session. These are workload-specific measurements with small run counts. ### Re-measured on the target stack at `e2c7075` The refactored implementation was re-measured on the stack this PR declares: vLLM `0.32.0` @ `5df63a7`, vLLM-Ascend `bd42eea0`, BF16 A2E transport, Attention-side gate, automatic communication selection, TP=1, eager (no CUDA/ACL graph), 4A4F on one A5 node. | Run | E2E (ms) | Prefill (token/s) | Output (token/s) | Total (token/s) | Failed | | --- | ---: | ---: | ---: | ---: | ---: | | 13:28 | 5,126.2 | 1,614.2 | 728.59 | 189,136 | 0 / 128 | | 13:34 | 5,146.5 | 1,607.9 | 726.69 | 188,643 | 0 / 128 | The two runs agree within 0.4%, so they are recorded as a repeat baseline rather than a single sample. GSM8K on the same configuration scored **89.92%** on the full 1,319-question evaluation. These numbers describe this configuration only: there is no same-stack reference implementation to compare against, so they are not evidence of a performance change from the refactor. The pre-refactor table above was measured on the previous stack and stays attributed to `828582a`. ### Placement re-measured on the target stack (`e2c7075`) Same node, same stack (vLLM `0.32.0` @ `5df63a7`, vLLM-Ascend `bd42eea0`), same workload (128 concurrency, 8,275 input / 32 output tokens per request), zero failed requests. The clean baseline is upstream main at `b093863`, which keeps the gate on the FFN side. | Mode | Clean baseline `b093863` (total token/s) | Attention-side gate `e2c7075` (total token/s) | Delta | | --- | ---: | ---: | ---: | | Eager | 165,345 | 189,136 / 188,643 | **+14.2%** (mean) | | Graph (`FULL_DECODE_ONLY`) | 364,627 / 362,563 | 354,731 / 353,972 | **-2.5%** (mean) | On this workload, Attention-side placement averages about **+14.2% higher total throughput in eager** and about **2.5% lower in graph**. That is a placement comparison across two commits on one stack; it is not a pure refactor cost, and it does not mean Attention-side placement is unsuitable for graph mode. Graph mode also raises this configuration's own throughput by **87.6%** over eager (189,136 -> 354,731 total token/s). GSM8K on the target stack scored **89.92%** in eager and **89.99%** in graph mode, both on the full 1,319-question evaluation. ## Baseline and scope Rebased onto main at `e58fdec`, which includes the merged #500 opaque routing-op fix. The retired `profile_run_eager` implementation and its wiring have been removed. #506 builds MXFP8 pass-through on this routing contract. [Incremental diff for this PR: `e58fdec...2c17cce`](Pingzii/afd-plugin@e58fdec...2c17cce) ## Verification CPU, measured at `2c17cce`: **59 passed, 2 skipped**. The focused set covers the routing-id op, the runtime installer wiring, the CAMP2P token-ID path, the routing payload transforms, and the new MoE component. The maintained CPU suite also passes. Two mutation checks were run on the refactored component before the rebase, and both produced the expected failures: - disabling the per-token state cleanup makes the MC2 finalize case fail (it observes state that should already be cleared); - disabling the comm restore makes the prepare-failure case fail (it observes the AFD comm left in place). Device evidence: the head `7430a13` starts on one A5 4A4F node with BF16 transport, `compute_gate_on_attention=true`, the CAMP2P connector, TP=1 and eager mode, and completes a generation request (`POST /v1/completions` returned 200). Accuracy and throughput were measured at `e2c7075`. The pre-refactor table above stays attributed to `828582a`. --------- Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com> Co-authored-by: 宋元盟 <songyuanmeng2@huawei.com>
## Purpose Enable native decode graphs for Async CAM DeepSeek-V4 target Attention and DSpark draft Attention. Reuse the vLLM/vLLM-Ascend graph managers and install AFD metadata at capture/replay boundaries for remote expert calls. ## Issue Related to #499; complements the F-side FULL support merged in #532. ## Scope - Target native graphs with an eager or native-graph DSpark draft; matching target/draft TP and independent F execution settings. - Preserve Attention-local FlashComm1/SP and native PD. P uses the upstream complete draft model and existing KV transfer; no new PD protocol is added. - Accept native `FULL`/`FULL_DECODE_ONLY` resolution. Remove graph-specific restrictions on compilation mode and model-free speculation; existing model-based draft/MTP checks remain. Device qualification uses mode 0. - Keep DBO excluded and draft graph dependent on target graph, as native target capture owns draft capture. Unequal target/draft TP, native kernels, quantization changes and lifecycle work remain outside this PR. ## Implementation Notes - Stage target metadata after native capture input preparation and before replay, preserving real and padded token counts. Send synchronous DP control only when a connector has a control plane; Async CAM has none. - Scope draft metadata to its own descriptor and query width. Reuse the existing physical KV block-size adapter and restore temporary methods/context on normal and exceptional exits. - Native managers retain ownership of graph buffers, buckets, attention/KV updates, sampling and fallback. No new ModelRunner, F executor or communication protocol is introduced. Preserve main's #500 routing fix. - Integrate on main `e58fdec5876782b9c6a03683416611deea42af92`, retaining its MegaMoE path and FFN two-stream removal. No F-runner, connector or native-operator changes are included in the PR diff. ## Test Plan / Test Result The conflict resolution at `fb4095d` merges main `26d8151` and preserves both Async CAM Attention graph validation and the synchronous CAMP2p Attention-side routing constraints from #503. Focused CPU checks passed **123 cases**, with **35 dependency-gated skips**. Changed-file pre-commit hooks and `git diff --check` passed. NPU results below are prior evidence; no new device run was performed for this merge. After the earlier main synchronization at `7fca98e`, focused Async CAM connector/session CPU checks pass **40 cases**; **3 graph test modules** are skipped because the host lacks native dependencies. Documentation hooks, local links and diff hygiene pass. Conflict resolutions retain the Graph capability descriptions and main's documentation reorganization. Attention graph implementation files are unchanged. The earlier **112 passed, 33 dependency-gated skips** and Ruff results at `822779f` are reused. The merge retains the graph control-plane test fields and main's `attention_step` connector context. Six pinned-runtime feature-validation checks and the earlier metadata, restoration, signature and device results below are reused pre-merge evidence. No new NPU experiment was run for conflict resolution. Configuration tests continue to admit PD/SP/native options and retain the model-based draft boundary and DBO rejection. Retained NPU comparisons used the pre-merge main-integrated execution code (`5afe9dd89314a36850cbb19e9a4e40d9706c3b83`, with the admission gates removed): | Combination | Fixed functional requests | Greedy output vs same-config eager | Successful target / draft replays per A rank | Padded draft replays per A rank | | --- | ---: | --- | ---: | ---: | | SP / FlashComm1 | 13 | All token-for-token identical | 73 / 86 | 86 | | Native PD + SP / FlashComm1 | 10 | All token-for-token identical | 82 / 83 | 83 | Actual SP flags were checked on both target and draft. PD used `MooncakeHybridConnector` with native full-draft P TP4. Cases cover one-token, 127/128/129-token and chunked 513-token prompts, concurrency and request reuse. PD eager/graph both recorded 405 drafted and 263 accepted tokens. In the SP comparison, the totals were 475/347 and 465/345 respectively; outputs still matched exactly, and concurrent scheduling was not fixed between arms. The pinned native configuration resolver was also executed directly: requested `FULL` resolves to `FULL_DECODE_ONLY` for DSV4's `UNIFORM_BATCH` backend. This was a configuration check, not another model experiment. Compilation mode 3 and model-free speculation admission were checked at the configuration seam only; their device execution is not qualified here. One formal measurement per arm used W4A8, A DP1/TP4, F EP4/TP1 FULL, five speculative tokens, 32 requests, 128 input and 128 output tokens, concurrency 4 and four client warmups. Each arm completed 32/32 requests and 4,096 output tokens. Functional replay probes were removed before timing; their counters remained unchanged afterward. No profiling ran during measurement. | Combination | Mean TPOT, eager → dual graph | Output throughput, eager → dual graph | Mean TTFT, eager → dual graph | | --- | ---: | ---: | ---: | | SP / FlashComm1 | 38.40 → 14.83 ms | 86.89 → 221.53 token/s | 412.96 → 254.95 ms | | Native PD + SP / FlashComm1 | 36.15 → 12.22 ms | 83.85 → 226.11 token/s | 1041.01 → 531.84 ms | Retained earlier non-PD, SP-off evidence: nine finite requests matched the reference token-for-token, with 51 target and 60 draft replays per A rank. Its single performance comparison measured TPOT 32.20 → 14.63 ms, throughput 102.62 → 226.70 token/s and TTFT 393.14 → 234.11 ms. That earlier measurement used `140369b873c34779d0d53a8af82aced3c47484eb` on an older main baseline; it was reused rather than rerun. Do not compare absolute numbers across the separate deployment experiments or attribute the combined gain to one graph. Pinned stack: vLLM `5df63a7203cdb9328d824a27b682e77c7b9928be`, vLLM-Ascend `bd42eea01d8312c4df469fabaed45f77a12df4d3`, CANN 9.2 and torch-npu 2.10.0.post7 on Ascend 910C/A3. Existing native binaries were reused unchanged. MegaMoE with A graphs, W8A8 A graphs, other KV connectors/topologies, GPU execution, DBO and task-level model accuracy are not newly qualified. Successful cleanup of owned processes is not a new graceful-shutdown qualification. ## Docs Impact Update the existing Attention, model/platform, Async CAM/FFN and DSpark recipe contracts. Distinguish actual implementation dependencies from device validation coverage. Experiment scripts, probes, raw logs and private evidence stay outside the patch. --- <details> <summary>Essential PR Checklist</summary> - [x] Purpose is clear and linked to public context. - [x] Scope is bounded. - [x] Compatibility with the pinned vLLM 0.32 / Ascend pair is considered. - [x] No upstream source checkouts are changed. - [x] Existing plugin classes and native graph managers are reused. - [x] Scoped native hooks are restored, documented and signature-checked. - [x] Runtime imports retain NPU boundaries and dependency guards. - [x] Executed, reused and unverified validation paths are distinguished. - [x] Documentation impact is stated. </details> --------- Signed-off-by: jiaran-king <jiaranran2@gmail.com> Signed-off-by: Zhou ziheng <jiaranran2@gmail.com>
What this fixes
The Attention-side gate reads the profile flag in plain Python:
Tracing specializes that branch on the value seen while profiling, so the compiled graph keeps applying profile-time balancing to live routing. Native Ascend avoids this because the same read sits inside the opaque
moe_forwardboundary.Change
The decision moves into a mutating custom op:
direct_register_custom_op(mutates_args=["topk_ids"])plus a fake implementation, so the flag is read at execution time;afd_plugin/routing_balance.py, so the compatibility layer no longer imports model code;No runner patch.
Evidence
CPU results at
110793d(earlier head): targeted tests 30 passed (recomputing the op through a fixture-scoped registration built from the production signature,Metauses the production fake, a no-guards replay reuses the first compiled callable with the old plain-Python branch as a negative control,opcheckcovers the mutating branch, Inductor case included); maintenance suite passes. A reversed check withmutates_args=()failsopcheck, so the mutation assertion is live.NPU results below were collected on the integrated branch at
e181f22, using the samecore routing-boundary implementation with the runner-level patch removed. This PR ports
that fix to
main; the current head passes 30 targeted CPU tests and the maintained CPU suite.Graph capturing finished in 8 secs).ACL graph replay is active ... (logged once).quant_mode=0, andFULL_DECODE_ONLY.Registration now also covers the standalone checkpoint-reference script after device
initialization. CPU gate tests use a shared, scoped fixture that registers the production op
and fake implementation. Against head
110793d, all 30 targeted tests pass, including the 13existing DSV4 gate tests; the maintained CPU suite also passes.
Compiled-artifact reuse on this head (
732d03a)tests/npu/check_routing_boundary_reuse.py, real NPU, production op registration and a realforward context. The stack is put into V2 extra-kwargs mode through its own entry point
(
sync_v2_extra_kwargs), and the log confirmsVLLM_USE_V2_MODEL_RUNNER=True is set; using Model Runner V2.Three assertions establish the layout before the reuse checks run:
_USE_V2_EXTRA_KWARGS is True);additional_kwargs(it raisedKeyErroron the legacy layout, so this isnot a pass-by-default);
_EXTRA_CTX.in_profile_run) agrees with what was written.Then: compile once while the profile flag is set and assert that first compiled call produced
balanced IDs; clear the flag, reuse the same compiled callable for two distinct non-balanced
inputs (both keep their own IDs, backend reports one compilation); capture an ACL graph with the
flag cleared and replay it twice over one static buffer, checking each replay's IDs outside the
captured region.
Device results on this head were provided by the contributor; the reviewer did not re-run them.
Still open: the V2 gate regression needs a full vLLM install to execute, and three failures in
tests/unit/model_executor/models/test_forward_context.pyare observed locally only as whole-fileskips, so their attribution is unconfirmed.
Notes
This supersedes the earlier approach in this PR, which replaced
NPUModelRunner.profile_runand disabled compilation for the profile pass. That copy is gone.Three failures in
tests/unit/model_executor/models/test_forward_context.pyare observed locally only as whole-file skips (no vLLM on this host). Their attribution is unconfirmed and is not claimed to be excluded here.