Skip to content

feat(npu): transport BF16 Attention-side routing to the FFN role - #503

Merged
jiangkuaixue123 merged 15 commits into
vllm-project:mainfrom
Pingzii:pr2/external-routing
Nov 10, 2026
Merged

jiangkuaixue123 merged 15 commits into
vllm-project:mainfrom
Pingzii:pr2/external-routing

Conversation

@Pingzii

@Pingzii Pingzii commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

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

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 one A5 4A4F node, Attention-side gate placement at e2c7075 delivered 14.2% higher total throughput in eager mode and 2.5% lower throughput under FULL_DECODE_ONLY than the FFN-side baseline at b093863 on the same software stack. Separate eager profiles showed a 495 µs/call shorter mean blocking wait for the FFN response (E2a) alongside an 18.5 µs/call increase in Attention-side MatMulV3 time, consistent with less serial overhead on the FFN return path. Under graph execution, the observed E2a means were both about 0.9 ms, with one DP rank missing from the Attention-side profile. The throughput runs and profiles were collected separately, and the comparison spans two commits and unequal profiling windows, so the measurements leave the standalone refactor effect and the contribution of individual stages unresolved.

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

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.

@Pingzii
Pingzii force-pushed the pr2/external-routing branch from 5fd5077 to 81569f9 Compare October 28, 2026 06:57

@ksiyuan ksiyuan 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.

Reviewed at head 828582a8 (base 7452d527). Draft PR, DCO only so far — no test-ready run has reported.

Findings first, ordered by severity. I read the per-commit split, so profile_run_eager.py / validation.py-profile bits are treated as #500 and not reviewed here as new work.

Findings

[P1] Gate-on-Attention is still rejected for the connector-local flag, so no documented config reaches the new path

afd_plugin/connectors/npu/camp2p.py:148

fail_if_unsupported_npu_afd_features now admits compute_gate_on_attention=true through the new guard (afd_plugin/compat/npu/feature_validation.py:65-75), then falls into extra_info.validate_supported() at feature_validation.py:84, which still raises:

AFD NPU runtime does not support compute_gate_on_attention=true yet

for the connector-local flag, unchanged from main. docs/npu/CAM_P2P_CONNECTOR_USER_GUIDE.md:164 states both the connector-local and the common field must be false, so the documented configuration aborts at startup. The only configuration that works is common true + connector-local false — the one the guide forbids — and nothing reconciles the two flags.

The new test cannot see this: tests/unit/compat/npu/test_sync_gate_feature_validation_cpu.py:28-49 swaps the class out for a _FakeExtraInfo whose validate_supported() is a no-op, so the assertion at :87 (extra_info.validated == 1) passes while the real class raises.

Smallest fix: delete the compute_gate_on_attention rejection from CAMP2PExtraInfo.validate_supported() — the new sync-gate guard is now the single source of truth for this mode — or, if the two flags are meant to stay independent, make the guard reject the connector-local flag explicitly with a message that says which one to change. Either way, drive the real CAMP2PExtraInfo through the validator in the CPU test.

[P2] The new public mode leaves its owner docs asserting the mode is unsupported

docs/design/module/connector_contracts.md:96

Still unchanged:

  • connector_contracts.md:96 — "the current runtime rejects gate-on-Attention"
  • docs/design/module/plugin_boundary.md:139 — "synchronous CAMP2P still requires false"
  • docs/design/module/execution_platforms.md:369 — compute_gate_on_attention=false
  • docs/npu/CAM_P2P_CONNECTOR_USER_GUIDE.md:146 and :164 — "Currently only false is supported" / "must remain false"

Only the patch-inventory row was updated. This is a new supported shape, so its owning pages need the new row, plus a launch recipe that names the exact flags — which matters precisely because of the P1 split above. Fix: update those five rows and add a working --additional-config example.

[P2] The copied AscendRoutedExperts.forward_impl has no upstream pointer and understates its deltas

afd_plugin/model_executor/models/npu/deepseek_v4.py:607

The docstring says the body "Mirrors AscendRoutedExperts.forward from the pinned vLLM-Ascend tag, with two differences", but the copy in _compute_external_routing_ffn also drops:

  • mix_placement shared-expert id concatenation
  • _record_v2_eplb_load
  • dynamic-EPLB heat collection
  • LoRA context sync
  • the moe_layer_index overflow guard for enable_npugraph_ex_static_kernel
  • return_with_event milestone events

Most fall outside the enforced matrix — the profile-run force-balancing omission is legitimately compensated in deepseek_v4_attention_gate.py:70-78, and the MRV2 validator rejects EPLB — but on the next vLLM-Ascend bump nobody can tell which omissions were deliberate. The surrounding file already uses # ### PATCH START: / END markers for upstream-derived code (:422, :426, :439, :458, :510), so the convention exists and was not applied here.

Related, same function boundary: _routed_experts_of (:377) converts AttributeError into a custom RuntimeError. AGENTS.md asks for the opposite — direct attribute access, so an upstream rename surfaces as the original error rather than as a plugin-invented one.

Fix: add # Upstream source: vllm_ascend/ops/fused_moe/routed_experts.py (8d4409d), AscendRoutedExperts.forward_impl to the docstring, mark each delta with PATCH markers, name the omitted behaviours, and read self.mlp.experts.routed_experts directly.

[P3] Duplicated guard whose only parameter is deleted

afd_plugin/validation.py:146

_fail_if_unsupported_sync_gate_runtime takes afd_config and immediately dels it; its entire body duplicates the DBO/ubatching check already enforced by _fail_if_unsupported_sync_attention_gate, which always runs for the same config. Two copies of one rule drift. Fix: drop the helper and the parameter, or keep one shared predicate and call it from both sites.

No-issue conclusions

Recorded so the next reviewer does not re-derive them:

  • Patch copies are faithful. I diffed both patched bodies statement-by-statement against the pinned 8d4409d PrepareAndFinalizeWithAllGather._prepare_with_dp_group and PrepareAndFinalizeWithMC2.prepare. Only the marked deltas differ, and with afd_external_routing is None the async CAM and gate-on-FFN paths execute the upstream logic unchanged.
  • Padding cannot misroute. Padding rows carry weight 0, so the pads' id value contributes nothing; gathered / split_rows reuse the activation's own collective and split, in the same row order.
  • Call contracts match. comm_method.prepare / finalize / quant_method.apply signatures and the reduce_results / padded_hidden_states_shape selection match native forward_impl.
  • The GPU twin change is inert, not drift: afd_requires_attention_routing is only ever true for CAMP2P, so v1/worker/ffn_model_runner.py forwarding recv_routing cannot change a CUDA run.

Validation gaps

Device behaviour is still unproven and the description says so: real replacement of both prepare classes, the Attention-side send plus shared-expert merge, FFN-side consumption, both comm paths end to end, the token-id regression, accuracy and throughput. The measurement table is explicitly cross-commit and historical, so it is not base/head evidence for this branch.

One specific thing worth confirming first on device: that the A2E operator round-trips the scales slot. csrc/npu/ascend_kernels/a2e/op_host/a2e.cpp:214-238 declares scales -> simulate_expert_scales as an I/O pair, but the token-id path that used that slot before never wrote or read a meaningful value, so this branch is the first consumer. If the operator does not preserve it, the FFN side gets all-zero gate weights and fails silently rather than loudly.

@ksiyuan ksiyuan 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.

Re-read after the description update. Head is still 828582a8 — no code changed, so the previous findings stand unchanged. This is a delta review of the new evidence only.

The correction is right

I recomputed the numbers against the frozen head:

  • git diff 1da04df..828582a8 --shortstat → 23 files, +2769 / -36, so the new cost figure is correct. The previous +2,959 / -36 was 190 lines high; good catch.
  • 3-dot against main → 25 files, +3205 / -28, consistent with what the description states.
  • Cost table: 1077 + 1690 + 2 = 2769 ✓
  • Test table: 10+4+5+15+16+7+6+6+30 = 99 ✓, and the per-file counts match the collected cases (test_external_routing_prepare.py 4, test_external_routing_mc2_prepare.py 5, test_camp2p_token_ids.py 16 exactly; the rest line up once parametrisation is expanded).
  • Ratios are arithmetically sound: 192,887/163,997 = +17.6%, 326,400/331,456 = -1.53%, 163,997/164,543 = -0.33%, 331,456/333,407 = -0.59%.

The A/B contract is now largely satisfied: hardware/placement, workload, graph-vs-eager, warmup/repetitions, metric definition and topology are all stated, and excluding the uncaptured dependency versions from every claim by quoting only same-session ratios is the right handling. Retiring the old +17.06% / -1.87% pair explicitly was the right call too.

The strongest new line is "gate on FFN (828582a)" versus the clean baseline. Both patches install unconditionally, so that arm is the runtime evidence that the patched-but-inactive prepare path costs nothing — which is the property the static review can only assert.

Two asks on the new evidence

[P2] Accuracy has no same-head control arm

The accuracy section compares gate-on-Attention (90.60% graph, 91.13% eager) against the model's historical range across earlier configurations (89.92-91.13). But the gate-on-FFN arm was already run on 828582a in that same session for throughput, and its GSM8K is the control that actually answers "did moving the gate change the numerics". A historical range cannot exclude a systematic routing error that happens to land inside it — and that is precisely the failure mode this PR has to exclude. Smallest fix: one gate-on-FFN GSM8K run on the same head and session, reported side by side.

[P3] The graph delta sits inside the stated noise band

The description states a within-arm spread of 0.4%-3.2% at n=2. Of the four quoted ratios, only +17.6% exceeds that band; -1.5%, -0.3% and -0.6% do not. Bolding "-1.5%" invites reading a measured regression into it. Putting the noise band into the comparison table and flagging which deltas clear it would state the graph result as "no measurable change" — which makes the +17.6% easier to accept, not harder.

Also missing from the manual-run evidence: the launch command per arm. perf-verification asks for environment, command and numbers; the environment and numbers are now there, the commands are not.

Carried forward, unchanged

Code is byte-identical to 828582a8, so these are not re-derived here:

  • [P1] afd_plugin/connectors/npu/camp2p.py:148 — CAMP2PExtraInfo.validate_supported() still raises for connector-local compute_gate_on_attention, after the new guard has already admitted it at feature_validation.py:84. The Implementation Notes text added in this edit ("limited to the sync connector, the DeepSeek V4 target and TP=1") does not mention the connector-local field either, so it still reads as if setting the common field is sufficient.
  • [P2] Five owner docs still state the mode is unsupported; the Docs Impact section still lists only compatibility_and_patches.md.
  • [P2] The two prepare patches and deepseek_v4.py:607 still carry no # Upstream source: pointer or PATCH markers. The new sentence "copy the pinned 8d4409d prepare bodies and add one difference" is exactly the information that belongs in those code comments, not only in the description.
  • [P3] afd_plugin/validation.py:146 — duplicated guard whose only parameter is deleted.

@jiangkuaixue123 jiangkuaixue123 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.

Follow-up on the configuration finding at 828582a8. The PR description now correctly directs users to the common afd.compute_gate_on_attention flag. The connector-local flag defaults to false, so the common flag alone can enable the feature. I would narrow the earlier P1 to a documentation fix: docs/npu/CAM_P2P_CONNECTOR_USER_GUIDE.md:146 and line 164 still say the common flag must be false, while afd_plugin/connectors/npu/camp2p.py:149 rejects setting the connector-local flag to true. Please show one supported TP=1 example that sets the common flag and omits the connector-local copy. Static re-review on the same head, no new NPU run.

@Pingzii
Pingzii force-pushed the pr2/external-routing branch from 828582a to cd45854 Compare November 4, 2026 08:48

@jiangkuaixue123 jiangkuaixue123 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.

Please scope the external-routing prepare implementation to AFD-owned classes; see the inline architectural suggestion.

the gathered routing back after it, and clears it in a ``finally``.
"""

PrepareAndFinalizeWithMC2.prepare = prepare

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.

Could we implement this as an AFDPrepareAndFinalizeWithMC2(PrepareAndFinalizeWithMC2) subclass instead of replacing the upstream class's prepare globally? This assignment affects every instance, including the one constructed by FusedMC2CommImpl, although the external-routing consumer only supports ALLGATHER and MC2. The upstream MC2CommImpl._get_prepare_finalize() already provides a construction seam: an AFD comm implementation could return the AFD subclass, selected only for the FFN external-routing path, while retaining the upstream initialization and finalize behavior. Please wire that selection into comm-method construction/registration; if an integration patch is unavoidable, keep it at that boundary rather than patching the computation method process-wide. Also consider passing external routing explicitly and returning the aligned routing from an AFD-specific prepare method, instead of the caller writing, reading back, and clearing afd_external_routing on the shared prepare object. This would make the data contract and ownership much clearer.

@Pingzii Pingzii Nov 6, 2026 •

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.

Addressed. AFDPrepareAndFinalizeWithMC2(PrepareAndFinalizeWithMC2) now provides prepare_external(), which takes the routing as an argument and returns the complete MoEPrepareOutput together with the aligned routing. It is selected through AFDMC2CommImpl._get_prepare_finalize() only on the external-routing FFN path, and the old external_routing_mc2_prepare.py patch is gone. See b669c0a.

@jiangkuaixue123 jiangkuaixue123 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.

The ALLGATHER implementation has the same architectural concern as the MC2 prepare patch. Please prefer inheritance/composition and avoid global patches wherever an extension seam is available.


if PrepareAndFinalizeWithAllGather._prepare_with_dp_group is prepare_with_dp_group:
return
PrepareAndFinalizeWithAllGather._prepare_with_dp_group = prepare_with_dp_group

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.

This has the same scoping issue as the MC2 prepare patch: assigning _prepare_with_dp_group on the upstream class changes the DP prepare path for every PrepareAndFinalizeWithAllGather instance in the process, even when external routing is disabled. Please prefer an AFDPrepareAndFinalizeWithAllGather subclass, selected through an AFD comm implementation's _get_prepare_finalize() only for the external-routing FFN path, and retain the upstream class for ordinary execution. The subclass can own routing alignment while preserving the corresponding finalize behavior. Please also avoid adding afd_external_routing to the upstream class; an explicit AFD prepare input/result would make the routing contract clearer than the caller's write/read/clear protocol. In general, please avoid monkey patches wherever inheritance or composition provides an extension seam. If construction/registration still needs a patch, keep it minimal at that boundary and document why it is unavoidable, rather than globally replacing the computation method.

@Pingzii Pingzii Nov 6, 2026 •

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.

Addressed the same way. AFDPrepareAndFinalizeWithAllGather(PrepareAndFinalizeWithAllGather) provides prepare_external() and the global PrepareAndFinalizeWithAllGather._prepare_with_dp_group = ... assignment is deleted, so ordinary execution keeps the upstream class. Selection goes through AFDAllGatherCommImpl._get_prepare_finalize(). See b669c0a.

@jiangkuaixue123

Copy link
Copy Markdown
Collaborator

Please fix precommit

@jiangkuaixue123 jiangkuaixue123 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.

Please keep the model integration focused on role/protocol selection and move external-routing MoE execution into an AFD-owned NPU component. Two inline suggestions below.

# Target Hash MoE takes explicit token IDs from the CAM transfer.
return self.mlp(hidden_states, input_ids=get_forward_context().input_ids)

def _compute_external_routing_ffn(

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.

Could we move this execution flow into an AFD-owned NPU MoE component, using inheritance/composition together with the AFD prepare/finalize implementations discussed above? This decoder-layer method currently mirrors AscendRoutedExperts.forward, selects concrete communication implementations, invokes quant_method.apply, and manages routing/mask/scale state across prepare and finalize. Those responsibilities belong with the MoE execution implementation; the model should select the role/protocol, validate its inputs, and delegate with hidden states and external routing. Please give that component an explicit routing input/result contract so the model no longer needs to write/read/clear prepare_finalize.afd_external_routing. The required computation does not disappear, but its state ownership and upstream coupling should be contained in that component rather than expanding this model file. Preserve the current prepare/finalize pairing and exception cleanup when refactoring.

@Pingzii Pingzii Nov 6, 2026 •

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.

Addressed. The prepare, expert execution and finalize flow now lives in afd_plugin/model_executor/models/npu/external_routing_moe.py::AFDExternalRoutingMoE, which owns the comm candidates, switches the forward-context comm for the call, restores it on every exit path, and clears the per-token state. The model only selects the role/protocol and delegates. See b669c0a.

return output


def _routed_experts_of(moe: Any) -> Any:

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.

Please remove this wrapper and access moe.experts.routed_experts directly at the appropriate typed boundary. It only wraps one upstream attribute access, introduces Any for both input and output, and converts an upstream AttributeError into a custom RuntimeError without recovery. Our development guidelines explicitly prefer direct access to vLLM/vLLM-Ascend members so upstream compatibility failures stay visible and static type checking can catch renamed attributes. This helper adds indirection without reducing meaningful complexity.

@Pingzii Pingzii Nov 6, 2026 •

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.

Addressed. _routed_experts_of is deleted; the component takes self.mlp.experts.routed_experts at construction, so a renamed upstream member surfaces as a plain AttributeError and static typing sees it. See b669c0a.

@Pingzii

Pingzii commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the architectural suggestions. We propose the following refactoring:

  • Introduce AFD-owned ALLGATHER and MC2 prepare/finalize subclasses, selected through AFD comm implementations only for the external-routing FFN path. Ordinary execution and FusedMC2 retain their upstream implementations.
  • Keep the native prepare interface intact and provide an explicit AFD-specific prepare_external entry point. It accepts hidden states and routing corresponding to the local input rows, and returns the complete native MoEPrepareOutput together with the aligned routing. Padding, gathering and sharding follow the activation's existing rules.
  • Move prepare, expert execution and finalize into an AFD-owned NPU MoE component. The model handles role/protocol selection and input validation, then delegates to that component.
  • Reuse constructed comm instances and preserve automatic communication selection. Remove the upstream afd_external_routing attribute and its write/read/clear protocol.
  • Preserve communication-specific prepare/finalize pairing. Finalize runs after successful expert execution; temporary state is cleaned up on both success and failure.
  • Remove _routed_experts_of and access the upstream member directly at the typed component boundary.

We will first check whether construction can be integrated without patching. If a construction hook still requires a patch, it will be narrowly scoped and documented. Validation will cover default-path isolation, routing alignment, failure cleanup, and eager/ACL Graph behavior.

Comment thread afd_plugin/compat/npu/runtime.py Outdated
raise RuntimeError(
"AFD NPU MLA graph patch requires the vLLM-Ascend MLA resolver",
)
# The routing patches replace the MoE prepare path, so they belong here rather

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.

Remove this file modify

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.

done

@@ -0,0 +1,159 @@
# SPDX-License-Identifier: Apache-2.0

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.

This file move to afd_plugin/connectors/npu/camp2p/external_routing.py

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.

done

@Pingzii
Pingzii force-pushed the pr2/external-routing branch from c05455e to 1adafa2 Compare November 4, 2026 15:47

@jiangkuaixue123 jiangkuaixue123 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.

Reviewed the external-routing refactor at 1adafa24. One runtime integration issue is noted inline against the README-pinned vLLM-Ascend revision bd42eea0. This was source review; the local environment has no PyTorch, vLLM, pytest, or NPU runtime, so I did not rerun the CPU or device suites.

from vllm_ascend.ascend_forward_context import _EXTRA_CTX

experts = self.routed_experts
comm = self._comm_for(_EXTRA_CTX.moe_comm_method)

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.

[P1] Resolve the selected comm with this layer's MoE config

At the README-pinned vLLM-Ascend bd42eea0, the MRV2 platform hook initializes moe_comm_method with get_moe_comm_method(moe_comm_type) (platform.py:725-744). The registry is now keyed by (comm_type, (num_experts, num_local_experts)), so that config-less lookup uses (0, 0) and returns None for a real DSV4 layer. Native AscendRoutedExperts.forward_impl resolves it again with self.moe_config before prepare (routed_experts.py:722-724), but this external-routing path bypasses that method. Consequently the first FFN forward reaches _comm_for(None) and raises the unsupported-comm error before either ALLGATHER or MC2 can execute.

Please select the prebuilt candidate from _EXTRA_CTX.moe_comm_type, or resolve the native comm with get_moe_comm_method(_EXTRA_CTX.moe_comm_type, experts.moe_config) first. Add a case with the comm type selected but the context's comm instance unset; the current fake always supplies an already-resolved instance and misses this startup failure.

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.

Thanks for catching this. The external-routing path now selects the prebuilt comm instance from moe_comm_type when the context has no comm instance. I added a regression test for that case. DCO and pre-commit pass on 7430a13, and an eager generation smoke test passed on A5 4A4F. Could you take another look when you have a chance?

@Pingzii
Pingzii force-pushed the pr2/external-routing branch from e2c7075 to d3a3424 Compare November 5, 2026 06:57
@Pingzii

Pingzii commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

Done in 2be2c8d. The documentation fix covers both lines you flagged and adds the example you asked for:

  • docs/npu/CAM_P2P_CONNECTOR_USER_GUIDE.md:146 — the common afd.compute_gate_on_attention row no longer says only false is supported; it states the supported shape (DeepSeek-V4, TP=1, no DBO or ubatching) and that both roles set it.
  • line 164 — the connector-local row now says the key does not control routing placement, and to leave it unset rather than mirroring the common flag.
  • new section Single-node 4A4F synchronous gate-on-Attention example (DeepSeek-V4, TP=1) — both roles start with afd.compute_gate_on_attention: true and connector_extra_config without the connector-local copy, FFN first, Attention second, and it notes that dropping --enforce-eager plus a --compilation-config block runs the same pair with ACL graphs.

Same commit also removed the DBO/ubatching rejection in afd_plugin/validation.py, which duplicated the NPU feature gate that both workers already call, and refreshed a model comment that still described prepare patches writing the routing back.

Checks at 2be2c8d: CPU 54 passed, 2 skipped (both skips need vLLM/torch_npu on the host); DCO and pre-commit pass.

@Pingzii

Pingzii commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in 13a8316. The rejection tests now cover DBO, ubatching, and TP > 1 for both roles at the NPU feature gate. The runner tests still verify that supported configurations are accepted. The deployment docs now describe the common flag and include the explicit FULL_DECODE_ONLY configuration.

Two follow-ups in c84fc88: the 4A4F example intro now describes the connector-local key the same way as the field table (kept for backwards compatibility, not a routing switch), and the PR body keeps the device smoke attributed to 7430a13, the commit it actually ran on.

@Pingzii
Pingzii force-pushed the pr2/external-routing branch from c84fc88 to efa170d Compare November 6, 2026 09:43
Carry the gate output the Attention side computes over the existing A2E ids slot,
with the top-k weights alongside the ids.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Use the transported top-k for routed experts instead of computing a second gate on
the FFN side, and keep shared experts on Attention.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Install the runtime wiring during worker start-up and cover the consumer on CPU-only
hosts, with the AFD-level gate switch as the only judge.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Replace the global prepare patches with AFD-owned prepare/finalize and comm
subclasses plus an AFD MoE component that owns prepare, expert execution and
finalize, with an explicit routing contract and scoped comm switching.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Query the native selector with MoECommType, treat a missing candidate as absent,
narrow component creation to the synchronous FFN role, and remove the unused
builder and stale comment.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Keep the routing payload next to the connector that carries it and point the
contract documentation at the new location.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
The MRV2 platform hook resolves moe_comm_method without a config, which misses the
(type, expert-count) registry key and leaves the instance unset on the first
forward. The comm type is always present, so the component now falls back to it and
picks the prebuilt candidate for that type instead of raising the unsupported-comm
error before either path can run.

Adds the startup case the fakes missed: comm type selected, context instance unset.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
The DSV4 layer hook now takes **_, so a caller that still passes group_list would have
been silently ignored instead of failing. Name the parameter again and reject a mixed
payload on the synchronous external-routing path.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
The CAMP2P guide still said the synchronous Attention-side gate was unsupported. It is now a
supported deployment, so the field matrix states the real supported shape and a 4A4F DeepSeek-V4
example starts both roles with compute_gate_on_attention=true and without the connector-local key.

Also drops the DBO/ubatching rejection that duplicated the NPU feature gate, and refreshes the
model comment that still described prepare patches writing the routing back.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
The runner validator no longer rejects DBO or ubatching, so its tests have to stop
requiring that rejection. The limit is enforced by the NPU feature gate, which both
workers run before the validator, and that entry now covers both roles.

Also drops the stale validation message claiming the synchronous connector needs
compute_gate_on_attention=false, states that the connector-local field is kept for
backwards compatibility and ignored for routing, records the graph flags in the
TP=1 example, and updates the design docs that still called the placement unsupported.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
@Pingzii
Pingzii force-pushed the pr2/external-routing branch from efa170d to 2c17cce Compare November 9, 2026 15:17
@github-actions github-actions Bot added documentation Improvements or additions to documentation Ascend Ascend NPU platform and related changes labels Nov 9, 2026
@@ -146,10 +146,10 @@ def aiv_num_for_role(self, role: str) -> int:
return self.core_num

def validate_supported(self) -> None:

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.

Why did this file become init.py

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.

I made camp2p a package so it could contain external_routing.py. The connector implementation now lives in camp2p/connector.py, and camp2p/__init__.py only re-exports the public API. Existing public imports still work.

6bcf39c: the implementation file is a 100% move, the tests that patch module attributes now target camp2p.connector (including the connector test, which skips on this host but would otherwise have missed the split), the worker test imports the private helper from the implementation module, and the connector contract points at connector.py. CPU: 59 passed, 4 skipped; the maintained suite passes.

Turning camp2p into a package put the whole connector in __init__.py. The implementation
now lives in camp2p/connector.py and __init__.py only re-exports the public API, so the
existing imports keep working while the package reads like a package.

Tests that patch module attributes now target the implementation module, the private
helper used by the worker test is imported from there, and the connector contract points
at connector.py.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
@lirx-pd

lirx-pd commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

The three-file split follows the gpu/deepep_m2n/ and models/npu/ patterns, so the direction is right. The findings are at the boundaries: the new package __init__ is the only eager re-export in the connector hierarchy, npu/ stops being flat while async_cam stays flat, and two owner docs were not brought along.


① [P2] afd_plugin/connectors/npu/camp2p/__init__.py:12

This is the only eager re-export in the connector hierarchy. connectors/__init__.py and connectors/npu/__init__.py both resolve names through a module-level __getattr__, and gpu/deepep_m2n/__init__.py documents the rule: "this package stays import-light so registering it costs nothing."

Because this import runs at module scope, import afd_plugin.connectors.npu.camp2p.external_routing now pulls in vLLM/torch_npu through connector.py, although external_routing.py imports only torch. The new test already pays for it — it loads that module via importlib.util.spec_from_file_location "because importing the afd_plugin.connectors package pulls vLLM in through its __init__".

Fix: make this lazy like npu/__init__.py (__getattr__ + TYPE_CHECKING). from afd_plugin.connectors.npu.camp2p import CAMP2pAFDConnector keeps working either way, since npu/__init__.__getattr__ already forwards through the package — compatibility does not require the eager import. If the package stays, also add the two new helpers to __all__.


② [P2] File naming and placement: connectors/npu/camp2p/connector.py:1, models/npu/external_routing_moe.py:1

1. Move camp2p/connector.py back to npu/camp2p.py. npu/ keeps one flat module per connector, named after it (camp2p.py, async_cam.py). async_cam is the precedent and solved the same problem the same way: connector flat, only the multi-file runtime in a package (async_cam_runtime/). A package for one connector file plus one payload file buys __init__ semantics and an extra import hop for no structure, and makes the synchronous connector the only subtree in that directory. Put the payload beside it as npu/camp2p_external_routing.py — the flat shape used for async_cam_layout.py / async_cam_dbo.py. If camp2p is expected to grow several runtime files, npu/camp2p_runtime/ is the consistent form; one file is not.

2. models/npu/external_routing_moe.py → deepseek_v4_external_routing_moe.py. All eight siblings carry their owner's prefix (deepseek_v4_attention_gate.py, deepseek_v4_shared_experts.py, deepseek_v2_attention_gate.py, async_cam_layout.py, …). The generic name also overstates scope: the component is built only for isinstance(self.mlp, native.DeepseekV4MoE). If it is meant to become generic, the other axis applies — #500 lifted a generic helper to afd_plugin/routing_balance.py rather than leaving a generic name inside a model package.


③ [P2] docs/design/module/model_integration.md — owner document untouched

  • No "Implementation evidence" row. That table lists model-local helpers one per row (npu/async_cam_dbo.py, npu/async_cam_layout.py, npu/deepseek_attention_metadata.py). npu/external_routing_moe.py is the same kind of module and has no documented owner without a row.
  • A sentence this PR makes false. "DeepSeek V4 Ascend boundary" still says FFN "owns the native gate and MoE" under synchronous CAMP2P; with compute_gate_on_attention=true it no longer does.

validation_paths already covers the new tests (tests/unit/model_executor/**), so only the row and that sentence need updating — state both supported shapes and the flag that selects them.


④ [P2] docs/design/module/connector_contracts.md — three statements now disagree with the code

  1. The "Connector-owned configuration" table still says the CAMP2P runtime "rejects gate-on-Attention", contradicting the paragraph you updated in the same section (supported: true on both roles, DeepSeek-V4, TP=1).
  2. "Payload and metadata ownership" describes AFDA2FTransferPayload without topk_ids / topk_weights, and without their exclusivity with input_ids on the shared A2E slot — that exclusivity is enforced in two places in the connector and belongs in this document.
  3. The ids-channel paragraph mentions only recv_input_ids / afd_requires_input_ids; the receive path now also has recv_routing / afd_requires_attention_routing.

Also: the Ascend CAMP2P "Focused validation" cell lists one unit file while the DeepEP M2N row lists four; the new external-routing contract tests should be added.

…mponent

The package entry eagerly imported connector.py, so importing the routing payload also
pulled vLLM and torch_npu in. Names now resolve through a module-level __getattr__, the
same shape as the parent packages, and the public imports keep working.

The external-routing MoE component belongs to DeepSeek-V4, so it now carries the owner
prefix like its siblings; its test moves with it. The payload test imports the package
normally instead of loading the file.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
The model integration owner document had no row for the external-routing MoE
component and still said the FFN role owns the native gate under synchronous
CAMP2P, which is only one of the two supported shapes.

The connector contract claimed the CAMP2P runtime rejects gate-on-Attention,
described the A2E slot without the routed top-k, and documented only the token-id
receive mode. Both documents now name the flag that selects the mode and the
payload the layer speaks.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
@Pingzii

Pingzii commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

The lazy exports, DeepSeek-V4 module rename, and documentation updates are in 6fa782f and 49e7999. I kept the camp2p package because an earlier review requested camp2p/external_routing.py, and the payload now imports without loading the connector or vLLM. If the flat layout is still preferred, I can move the payload to npu/camp2p_external_routing.py in a follow-up commit.

Details:

  • 6fa782f — camp2p/__init__.py resolves names through a module-level __getattr__ (TYPE_CHECKING for static visibility, __all__ unchanged), so import ...camp2p.external_routing no longer loads the connector, vLLM, or torch_npu. The payload test now imports the package normally instead of loading the file. external_routing_moe.py and its test moved to deepseek_v4_external_routing_moe.py / test_deepseek_v4_external_routing_moe_cpu.py.
  • 49e7999 — model_integration.md gets the external-routing MoE evidence row and states both synchronous shapes with the flag that selects them; connector_contracts.md no longer claims the runtime rejects gate-on-Attention, documents the topk_ids/topk_weights payload and its exclusivity with input_ids on the A2E slot, names recv_routing / afd_requires_attention_routing, and lists the new contract tests.

Checks at 49e7999: CPU 59 passed, 3 skipped; maintained suite passes; DCO, label and pre-commit pass.

@jiangkuaixue123
jiangkuaixue123 merged commit b33e20d into vllm-project:main Nov 10, 2026
3 checks passed
Pingzii added a commit to Pingzii/afd-plugin that referenced this pull request Nov 11, 2026
Three cases restated behaviour another seam already pins: the two
`compute_ffn_output` cases re-bound the same extracted methods that the runner
case drives through the real `_ffn_forward`, and the component-level row-order
case asserted the same bytes and layout as that runner case. They are gone.

Three pairs with identical setup became parametrize tables, keeping every
scenario: the pair contract now covers E4M3 without a scale, a scale next to
BF16, E5M2, and a scale with the wrong row count in one table; MC2 dispatch
covers an unset and an explicit pre-quantized comm mode; the control-payload
check covers 4/0, 0/4 and 4/4.

The BF16 no-scale assertion moved onto the vllm-project#503 routing case, which now states
`torch.bfloat16` explicitly instead of relying on the float32 default, and the
startup handshake case now also pins that `p2p` is built after the collective.

Mutation re-checked after the change: deleting the runner's
`activation_scale` forwarding still fails both the ALLGATHER and the MC2
implementation of `test_the_ffn_runner_carries_the_received_scale_to_the_experts`.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Pingzii added a commit to Pingzii/afd-plugin that referenced this pull request Nov 11, 2026
Three cases restated behaviour another seam already pins: the two
`compute_ffn_output` cases re-bound the same extracted methods that the runner
case drives through the real `_ffn_forward`, and the component-level row-order
case asserted the same bytes and layout as that runner case. They are gone.

Three pairs with identical setup became parametrize tables, keeping every
scenario: the pair contract now covers E4M3 without a scale, a scale next to
BF16, E5M2, and a scale with the wrong row count in one table; MC2 dispatch
covers an unset and an explicit pre-quantized comm mode; the control-payload
check covers 4/0, 0/4 and 4/4.

The BF16 no-scale assertion moved onto the vllm-project#503 routing case, which now states
`torch.bfloat16` explicitly instead of relying on the float32 default, and the
startup handshake case now also pins that `p2p` is built after the collective.

Mutation re-checked after the change: deleting the runner's
`activation_scale` forwarding still fails both the ALLGATHER and the MC2
implementation of `test_the_ffn_runner_carries_the_received_scale_to_the_experts`.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Pingzii added a commit to Pingzii/afd-plugin that referenced this pull request Nov 11, 2026
Three cases restated behaviour another seam already pins: the two
`compute_ffn_output` cases re-bound the same extracted methods that the runner
case drives through the real `_ffn_forward`, and the component-level row-order
case asserted the same bytes and layout as that runner case. They are gone.

Three pairs with identical setup became parametrize tables, keeping every
scenario: the pair contract now covers E4M3 without a scale, a scale next to
BF16, E5M2, and a scale with the wrong row count in one table; MC2 dispatch
covers an unset and an explicit pre-quantized comm mode; the control-payload
check covers 4/0, 0/4 and 4/4.

The BF16 no-scale assertion moved onto the vllm-project#503 routing case, which now states
`torch.bfloat16` explicitly instead of relying on the float32 default, and the
startup handshake case now also pins that `p2p` is built after the collective.

Mutation re-checked after the change: deleting the runner's
`activation_scale` forwarding still fails both the ALLGATHER and the MC2
implementation of `test_the_ffn_runner_carries_the_received_scale_to_the_experts`.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
jiangkuaixue123 pushed a commit that referenced this pull request Nov 12, 2026
)

## What this fixes

`camp2p` resolves its public names through a package-level `__getattr__`
(#503). The connector factory still read the class out of the module
dictionary:

```python
connector_cls = vars(module)[class_name]
```

For a lazily exported class that dictionary is empty, so every CAMP2P
start fails before the worker can build the connector:

```
File "afd_plugin/connectors/factory.py", line 40, in loader
    connector_cls = vars(module)[class_name]
KeyError: 'CAMP2pAFDConnector'
```

## Change

Read the class with `getattr`, which follows the `__getattr__` hook and
behaves exactly as before for eagerly exported modules.

The regression case moved into
`tests/unit/connectors/test_p2p_connector.py` with the other factory
tests: it registers a temporary connector backed by a module that only
answers through `__getattr__`, and registers into a copied `_registry`
so the temporary entry does not outlive the test. The missing-name case
was dropped; the standalone test file is gone.

## Validation

- The moved case fails with the old `vars()` lookup.
- `tests/unit/connectors/test_p2p_connector.py` and the maintained CPU
suite pass.
- DCO, label and pre-commit pass.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Co-authored-by: 宋元盟 <songyuanmeng2@huawei.com>
Pingzii added a commit to Pingzii/afd-plugin that referenced this pull request Nov 12, 2026
Three cases restated behaviour another seam already pins: the two
`compute_ffn_output` cases re-bound the same extracted methods that the runner
case drives through the real `_ffn_forward`, and the component-level row-order
case asserted the same bytes and layout as that runner case. They are gone.

Three pairs with identical setup became parametrize tables, keeping every
scenario: the pair contract now covers E4M3 without a scale, a scale next to
BF16, E5M2, and a scale with the wrong row count in one table; MC2 dispatch
covers an unset and an explicit pre-quantized comm mode; the control-payload
check covers 4/0, 0/4 and 4/4.

The BF16 no-scale assertion moved onto the vllm-project#503 routing case, which now states
`torch.bfloat16` explicitly instead of relying on the float32 default, and the
startup handshake case now also pins that `p2p` is built after the collective.

Mutation re-checked after the change: deleting the runner's
`activation_scale` forwarding still fails both the ALLGATHER and the MC2
implementation of `test_the_ffn_runner_carries_the_received_scale_to_the_experts`.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
Pingzii added a commit to Pingzii/afd-plugin that referenced this pull request Nov 13, 2026
Three cases restated behaviour another seam already pins: the two
`compute_ffn_output` cases re-bound the same extracted methods that the runner
case drives through the real `_ffn_forward`, and the component-level row-order
case asserted the same bytes and layout as that runner case. They are gone.

Three pairs with identical setup became parametrize tables, keeping every
scenario: the pair contract now covers E4M3 without a scale, a scale next to
BF16, E5M2, and a scale with the wrong row count in one table; MC2 dispatch
covers an unset and an explicit pre-quantized comm mode; the control-payload
check covers 4/0, 0/4 and 4/4.

The BF16 no-scale assertion moved onto the vllm-project#503 routing case, which now states
`torch.bfloat16` explicitly instead of relying on the float32 default, and the
startup handshake case now also pins that `p2p` is built after the collective.

Mutation re-checked after the change: deleting the runner's
`activation_scale` forwarding still fails both the ALLGATHER and the MC2
implementation of `test_the_ffn_runner_carries_the_received_scale_to_the_experts`.

Signed-off-by: 宋元盟 <songyuanmeng2@huawei.com>
jiaran-king added a commit that referenced this pull request Nov 13, 2026
Keep Async CAM Attention graph validation from PR #545 and the synchronous CAMP2p Attention-side routing validation from main (#503).

Signed-off-by: Zhou ziheng <jiaranran2@gmail.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

Ascend Ascend NPU platform and related changes documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants