Skip to content

fix(npu): keep profile routing decisions inside an opaque op - #500

Merged
jiangkuaixue123 merged 18 commits into
vllm-project:mainfrom
Pingzii:pr1/profile-pass-compile
Nov 4, 2026
Merged

jiangkuaixue123 merged 18 commits into
vllm-project:mainfrom
Pingzii:pr1/profile-pass-compile

Conversation

@Pingzii

@Pingzii Pingzii commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

What this fixes

The Attention-side gate reads the profile flag in plain Python:

if force_balanced_topk_ids_enabled() or _EXTRA_CTX.in_profile_run:
    topk_ids = _force_balanced_topk_ids(...)

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_forward boundary.

Change

The decision moves into a mutating 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)
  • registered with direct_register_custom_op(mutates_args=["topk_ids"]) plus a fake implementation, so the flag is read at execution time;
  • the gate GEMM and topk stay inside the compiled region, and nothing disables compilation;
  • the two call sites keep their previous semantics (v4: profile only, v2: its existing explicit switch);
  • the pure tensor helper moves to afd_plugin/routing_balance.py, so the compatibility layer no longer imports model code;
  • registration happens in the existing worker-side patch installer.

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, Meta uses the production fake, a no-guards replay reuses the first compiled callable with the old plain-Python branch as a negative control, opcheck covers the mutating branch, Inductor case included); maintenance suite passes. A reversed check with mutates_args=() fails opcheck, so the mutation assertion is live.

NPU results below were collected on the integrated branch at e181f22, using the same
core 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.

  • Standalone NPU check: profile mode writes balanced IDs; live IDs are preserved when profile mode is disabled.
  • Graph capture completes 5/5 (Graph capturing finished in 8 secs).
  • Runtime confirms first replay: ACL graph replay is active ... (logged once).
  • Full GSM8K (1,319 samples): 90.14, with gate-on-A, quant_mode=0, and FULL_DECODE_ONLY.
  • Same-day throughput: 309,221 median for the replacement (n=2), versus 311,978 for the patch-based state (n=1), a difference of -0.9% with limited samples.

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 13
existing 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 real
forward context. The stack is put into V2 extra-kwargs mode through its own entry point
(sync_v2_extra_kwargs), and the log confirms VLLM_USE_V2_MODEL_RUNNER=True is set; using Model Runner V2.

Three assertions establish the layout before the reuse checks run:

  • the cache reports V2 mode (_USE_V2_EXTRA_KWARGS is True);
  • the write lands in additional_kwargs (it raised KeyError on the legacy layout, so this is
    not a pass-by-default);
  • the op's read path (_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.

eager compiled reuse: OK
acl graph reuse: OK
ALL CHECKS PASSED

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.py are observed locally only as whole-file
skips, so their attribution is unconfirmed.

Notes

This supersedes the earlier approach in this PR, which replaced NPUModelRunner.profile_run and disabled compilation for the profile pass. That copy is gone.

Three failures in tests/unit/model_executor/models/test_forward_context.py are observed locally only as whole-file skips (no vLLM on this host). Their attribution is unconfirmed and is not claimed to be excluded here.

Copy link
Copy Markdown
Collaborator

Could we fix the compilation boundary in AFD's Attention-side routing instead of replacing the upstream NPUModelRunner.profile_run?

In the pinned stack, native Ascend reads in_profile_run inside the opaque moe_forward / moe_forward_shared custom-op boundary. AFD bypasses AscendRoutedExperts and introduces that Python branch directly in its Attention-side routing path, where tracing can specialize it. This appears to be the boundary we should restore.

disable_compilation does restore the previous state on exit, so this does not permanently disable compilation. However, disabling compilation for the entire profile pass changes the upstream startup compilation timing and profiles the eager path. Copying the upstream method and adding installation, conflict detection, and global import state also makes a small AFD-specific fix harder to understand and maintain.

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 in_profile_run read inside that boundary so it is evaluated at execution time, while preserving the upstream profiling/compilation lifecycle. The boundary should stay narrow to limit lost compiler optimizations.

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.

@Pingzii

Pingzii commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

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.

@Pingzii

Pingzii commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

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 direct_register_custom_op(mutates_args=["topk_ids"]) plus a fake implementation, and called from both gate paths. The flag is therefore read at execution time, the gate GEMM and topk stay inside the compiled region, and no part of the profile pass disables compilation. The wait for in_profile_run moved from Python into the op; the two call sites keep their previous semantics (v4: profile only, v2: its existing explicit switch).

What this PR becomes. The profile_run replacement, its install point, the disable_compilation wrapper, the copied upstream method, and the corresponding test and documentation entries are removed (~430 lines deleted). The op is registered during worker initialization through the existing patch installer, so no new import path is introduced.

Validation so far (CPU only). A regression loads the real op through a fixture-scoped registration (infer_schema from the production signature plus _scoped_library), registers the production fake as the Meta kernel, reuses the first compiled callable to emulate a no-guards replay, and keeps the old plain-Python branch as a negative control; opcheck exercises the mutating branch so a wrong mutates_args fails. Targeted tests pass (20) and the maintenance suite passes.

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 main, say so and I will move it.

@Pingzii

Pingzii commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Device checks are in for the replacement implementation (opaque routing-id boundary, profile_run patch removed). Configuration: single host A5 4A4F, Attention DP4/TP1, compute_gate_on_attention=true, quant_mode=0, automatic routing, no DBO, cudagraph_mode=FULL_DECODE_ONLY, chunk 3072, concurrency 128, 8275 in / 32 out.

1. Profile balancing and live routing through the op

Standalone NPU check against the registered op, read outside any capture:

profile: [[0,1],[2,3],[4,5],[6,7],[0,1],[2,3],[4,5],[6,7]]      # i % num_experts
live[5]: [[5,6],[7,0],[1,2],[3,4],...]  unchanged: True

With in_profile_run=True the op writes the balanced sequence; with it false a non-balanced input is left untouched. The second line is the discriminating case (the first input happened to already be balanced, so it proves nothing on its own).

2. Compilation, capture and replay in the served model

Capturing CUDA graphs (FULL): 100%|...| 5/5 [00:07<00:00, 1.41s/it]
Graph capturing finished in 8 secs, took 0.18 GiB
ACL graph replay is active for the V2 target model (logged once)   # all four workers
POST /v1/completions HTTP/1.1" 200 OK

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:

Checkout State Runs Throughput
ee9b0ed (branch + operator) old profile_run patch present 1 311,978
e181f22 (this implementation) patch removed, boundary op added 2 312,489 / 305,954 (median 309,221)

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 quant_mode=0; the MXFP8 transport path was not exercised here. The standalone check above shows the op's behaviour in both states, but it does not yet provide device evidence for two consecutive inputs reusing a single compiled artifact; that case stays open.

@Pingzii
Pingzii force-pushed the pr1/profile-pass-compile branch from 1da04df to f9377f2 Compare November 4, 2026 06:07
@Pingzii Pingzii changed the title fix(npu): keep the profile pass out of compilation fix(npu): keep profile routing decisions inside an opaque op Nov 4, 2026

Copy link
Copy Markdown
Collaborator

Re-reviewed head 110793d9fbab30ecbb15eee065700c5e8fa9c577. The earlier concern about replacing profile_run is resolved by the narrow routing custom-op boundary. Two items remain:

[P2] Register the op in the existing V2 gate regression test. The new unconditional call at deepseek_v2_attention_gate.py:169 is also reached by test_attention_gate_balances_only_profile_routing. That test directly calls the gate without initializing an AFD worker or requesting registered_routing_ids_op. In a fresh test process with the Ascend dependencies available, the op is therefore unregistered; hosts without those dependencies skip the test and hide the regression. Please use the shared CPU registration fixture for this test, as the V4 tests now do, and run the test individually. This finding is based on the call/registration paths; I did not execute the fork's tests locally.

Remaining NPU validation: the reported device results are from integrated commit e181f22, rather than this PR head, and the PR explicitly leaves reuse of one compiled artifact open. Please add a focused check on the current head and pinned stack: compile through the profile path with balancing enabled, then disable the profile flag and run two distinct, non-balanced live inputs through that same compiled artifact without recompilation. Verify that profiling produces balanced IDs and both live executions preserve their respective routing IDs. For ACL graph coverage, capture with profile mode disabled and replay with the two different inputs, checking routing output for each replay. Record the exact commit, environment, command, and assertions/results. The standalone eager-op check, successful capture, and first-replay log do not establish this specific reuse behavior.

@jiangkuaixue123

Copy link
Copy Markdown
Collaborator

Please fix DCO and pre-commit

Copy link
Copy Markdown
Collaborator

Follow-up review of 2a8f1fa96e5dc65532366df568086fd4ced206a7: the V2 gate test now requests the registration fixture, resolving the previous registration omission. Remaining findings and layout suggestions:

  1. [P2] Compile under the profile context in the NPU regression. At check_routing_boundary_reuse.py:79, _compile_once() runs with in_profile_run=False. The preceding True context only executes the eager op. This misses the original failure mode: specializing the balancing branch during profiling and reusing that artifact for live requests. Even the old plain-Python branch could pass this scenario. Compile and verify balanced IDs under the True context, then switch to False and reuse that exact callable for both distinct live inputs. Keep the ACL replay check, and report the corrected check's results on the current head and pinned stack.

  2. [P2] Consolidate the duplicated registration fixtures. test_routing_ids.py:33 retains a local balance_op fixture that duplicates the schema inference, CPU/Meta registration, module replacement, and restoration in tests/unit/fakes/routing_ids_op.py. Future registration or cleanup changes would need to be applied twice, contrary to the shared-fake convention in AGENTS.md. Use one shared fixture, exposing the production module and op as needed by the different tests.

  3. Fold the small balancing helper into the NPU routing module. afd_plugin/routing_balance.py has only one production caller, compat/npu/routing_ids.py, and the helper is short. Moving it out of model code avoids the reverse dependency, but adding a package-root module is unnecessary. Please keep the helper alongside the custom op in compat/npu/routing_ids.py; this preserves the dependency direction and keeps this NPU-specific behavior together without introducing another abstraction.

  4. Complete the ownership and test-entry documentation. Keeping the op in compat/npu/routing_ids.py, its unit tests in tests/unit/compat/npu/, and the shared fixture in tests/unit/fakes/ is reasonable. Add the new production module to the relevant docs/design/module/ ownership paths and describe its registration lifecycle. tests/npu/ is also the right location for the device check: if it remains a standalone check_*.py script, document its invocation in tests/npu/README.md; if it is intended as a maintained regression, prefer a discoverable test_routing_ids.py with the appropriate dependency/device guards.

The device evidence still references integrated commit e181f22; I have not seen results for the corrected profile-to-live reuse check on this head. This review was static; I did not execute the fork's code locally.

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>
@Pingzii
Pingzii force-pushed the pr1/profile-pass-compile branch from c04dec6 to f06e43b Compare November 4, 2026 07:58
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>
@jiangkuaixue123

Copy link
Copy Markdown
Collaborator

LGTM

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
@jiangkuaixue123
jiangkuaixue123 merged commit 652e249 into vllm-project:main Nov 4, 2026
2 checks passed
Pingzii added a commit to Pingzii/afd-plugin that referenced this pull request Nov 4, 2026
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>
jiangkuaixue123 pushed a commit that referenced this pull request Nov 10, 2026
## 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>
jiangkuaixue123 pushed a commit that referenced this pull request Nov 13, 2026
## 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>
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