Repository navigation
feat(npu): transport BF16 Attention-side routing to the FFN role - #503
Conversation
5fd5077 to
81569f9
Compare
ksiyuan
left a comment
There was a problem hiding this comment.
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 requiresfalse"docs/design/module/execution_platforms.md:369—compute_gate_on_attention=falsedocs/npu/CAM_P2P_CONNECTOR_USER_GUIDE.md:146and:164— "Currently onlyfalseis supported" / "must remainfalse"
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_placementshared-expert id concatenation_record_v2_eplb_load- dynamic-EPLB heat collection
- LoRA context sync
- the
moe_layer_indexoverflow guard forenable_npugraph_ex_static_kernel return_with_eventmilestone 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
8d4409dPrepareAndFinalizeWithAllGather._prepare_with_dp_groupandPrepareAndFinalizeWithMC2.prepare. Only the marked deltas differ, and withafd_external_routing is Nonethe 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_rowsreuse the activation's own collective and split, in the same row order. - Call contracts match.
comm_method.prepare/finalize/quant_method.applysignatures and thereduce_results/padded_hidden_states_shapeselection match nativeforward_impl. - The GPU twin change is inert, not drift:
afd_requires_attention_routingis only ever true for CAMP2P, sov1/worker/ffn_model_runner.pyforwardingrecv_routingcannot 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
left a comment
There was a problem hiding this comment.
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 / -36was 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.py4,test_external_routing_mc2_prepare.py5,test_camp2p_token_ids.py16 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-localcompute_gate_on_attention, after the new guard has already admitted it atfeature_validation.py:84. TheImplementation Notestext 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 Impactsection still lists onlycompatibility_and_patches.md. - [P2] The two prepare patches and
deepseek_v4.py:607still carry no# Upstream source:pointer or PATCH markers. The new sentence "copy the pinned8d4409dprepare 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
left a comment
There was a problem hiding this comment.
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.
828582a to
cd45854
Compare
jiangkuaixue123
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
Please fix precommit |
jiangkuaixue123
left a comment
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
Thanks for the architectural suggestions. We propose the following refactoring:
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. |
| 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 |
There was a problem hiding this comment.
Remove this file modify
| @@ -0,0 +1,159 @@ | |||
| # SPDX-License-Identifier: Apache-2.0 | |||
There was a problem hiding this comment.
This file move to afd_plugin/connectors/npu/camp2p/external_routing.py
c05455e to
1adafa2
Compare
jiangkuaixue123
left a comment
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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?
e2c7075 to
d3a3424
Compare
|
Done in
Same commit also removed the DBO/ubatching rejection in Checks at |
|
Addressed in Two follow-ups in |
c84fc88 to
efa170d
Compare
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>
efa170d to
2c17cce
Compare
| @@ -146,10 +146,10 @@ def aiv_num_for_role(self, role: str) -> int: | |||
| return self.core_num | |||
|
|
|||
| def validate_supported(self) -> None: | |||
There was a problem hiding this comment.
Why did this file become init.py
There was a problem hiding this comment.
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>
① [P2]
② [P2] File naming and placement:
③ [P2]
④ [P2]
|
…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>
|
The lazy exports, DeepSeek-V4 module rename, and documentation updates are in Details:
Checks at |
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>
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>
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>
) ## 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>
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>
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>
## 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>
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
int32and top-k weights asfloat32through the existing ids channel.AFDPrepareAndFinalizeWithAllGatherandAFDPrepareAndFinalizeWithMC2provide
prepare_external, which takes the routing for the local input rows and returns the completeMoEPrepareOutputtogether with the aligned routing. AFD comm subclasses override only_get_prepare_finalize(), so the native selector still decides which path runs and ordinary execution keepsthe upstream classes. There are no global prepare patches left.
upstream class, and the module no longer reads or clears an
afd_external_routingattribute.AFDExternalRoutingMoEbuilds its comm candidates during model initialization, wherethe 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.
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
828582aon one A5 4A4F node during the same evening session, with BF16 A2E transport and automatic communication selection:828582a)828582a)bcc0126)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 usesFULL_DECODE_ONLYwith 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
e2c7075The refactored implementation was re-measured on the stack this PR declares: vLLM
0.32.0@
5df63a7, vLLM-Ascendbd42eea0, BF16 A2E transport, Attention-side gate, automaticcommunication selection, TP=1, eager (no CUDA/ACL graph), 4A4F on one A5 node.
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-Ascendbd42eea0), 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.b093863(total token/s)e2c7075(total token/s)FULL_DECODE_ONLY)On one A5 4A4F node, Attention-side gate placement at
e2c7075delivered 14.2% higher total throughput in eager mode and 2.5% lower throughput underFULL_DECODE_ONLYthan the FFN-side baseline atb093863on 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-sideMatMulV3time, consistent with less serial overhead on the FFN return path. Under graph execution, the observedE2ameans 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 retiredprofile_run_eagerimplementation and its wiring have been removed. #506 builds MXFP8 pass-throughon this routing contract.
Incremental diff for this PR:
e58fdec...2c17cceVerification
CPU, measured at
2c17cce: 59 passed, 2 skipped. The focused set covers the routing-id op, the runtime installerwiring, 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:
be cleared);
Device evidence: the head
7430a13starts on one A5 4A4F node with BF16 transport,compute_gate_on_attention=true, the CAMP2P connector, TP=1 and eager mode, and completes a generationrequest (
POST /v1/completionsreturned 200). Accuracy and throughput were measured ate2c7075.The pre-refactor table above stays attributed to
828582a.