diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I20.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I20.json new file mode 100644 index 00000000..a0e8defa --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I20.json @@ -0,0 +1,47 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v4.py:L44-L61", + "text": "def _checkpoint_weight_roles(name: str) -> frozenset[str]:\n \"\"\"Classify a native DeepSeek-V4 checkpoint path by execution owner.\"\"\"\n if name in {\n \"hc_head_fn\",\n \"hc_head_base\",\n \"hc_head_scale\",\n \"model.hc_head_fn\",\n \"model.hc_head_base\",\n \"model.hc_head_scale\",\n }:\n return _ATTENTION_ROLE\n layer_path = _weight_layer_path(name)\n if layer_path is None:\n return _BOTH_ROLES\n _, stage = layer_path\n if stage == \"ffn\":\n return _FFN_ROLE\n return _ATTENTION_ROLE" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v4.py:L148-L167", + "text": " # ### PATCH START: replace the remote stage with a parameter-free proxy.\n if afd_config.role == \"attention\":\n self.attn = native._select_dsv4_attn_cls(vllm_config)(\n vllm_config,\n prefix=f\"{prefix}.attn\",\n topk_indices_buffer=topk_indices_buffer,\n aux_stream_list=aux_stream_list,\n )\n self.ffn = RemoteDeepseekV4FFN(layer_idx=layer_idx)\n elif afd_config.role == \"ffn\":\n self.attn = native.PPMissingLayer()\n self.ffn = native.DeepseekV4MoE(vllm_config, prefix=f\"{prefix}.ffn\")\n else:\n raise ValueError(f\"unsupported AFD role {afd_config.role!r}\")\n # ### PATCH END\n\n # ### PATCH START: mHC state and normalization are Attention-owned.\n if afd_config.role == \"ffn\":\n return\n # ### PATCH END" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v4.py:L217-L220", + "text": " # ### PATCH START: prohibit accidental execution on the FFN worker.\n if isinstance(self.attn, native.PPMissingLayer):\n raise RuntimeError(\"DeepSeek-V4 decoder forward is Attention-owned\")\n # ### PATCH END" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v4.py:L325-L345", + "text": " # ### PATCH START: validate the deliberately narrow first release.\n nn.Module.__init__(self)\n self.afd_config = parse_afd_config(vllm_config, validate=False)\n if native.current_platform.device_type != \"cuda\":\n raise RuntimeError(\"AFD DeepSeek-V4 supports CUDA only\")\n if self.afd_config.connector != \"P2pNcclAFDConnector\":\n raise RuntimeError(\n \"AFD DeepSeek-V4 requires the synchronous P2pNcclAFDConnector\",\n )\n if self.afd_config.compute_gate_on_attention:\n raise RuntimeError(\n \"AFD DeepSeek-V4 does not support compute_gate_on_attention\",\n )\n parallel_config = vllm_config.parallel_config\n if parallel_config.pipeline_parallel_size != 1:\n raise RuntimeError(\"AFD DeepSeek-V4 does not support PP\")\n if parallel_config.use_sequence_parallel_moe:\n raise RuntimeError(\"AFD DeepSeek-V4 does not support SP MoE\")\n if parallel_config.enable_eplb:\n raise RuntimeError(\"AFD DeepSeek-V4 does not support EPLB\")\n # ### PATCH END" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v4.py:L406-L423", + "text": " # ### PATCH START: final mHC state is constructed only on Attention.\n if self.afd_config.role == \"attention\":\n self.hc_head_fn = nn.Parameter(\n torch.empty(self.hc_mult, self.hc_dim, dtype=torch.float32),\n requires_grad=False,\n )\n self.hc_head_base = nn.Parameter(\n torch.empty(self.hc_mult, dtype=torch.float32),\n requires_grad=False,\n )\n self.hc_head_scale = nn.Parameter(\n torch.empty(1, dtype=torch.float32),\n requires_grad=False,\n )\n else:\n self.hc_head_fn = None\n self.hc_head_base = None\n self.hc_head_scale = None" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v4.py:L446-L466", + "text": " def get_experts_layer_indices(self) -> tuple[int, ...]:\n return tuple(range(int(self.config.num_hidden_layers)))\n\n def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:\n \"\"\"Return native expert mappings only where real experts are owned.\"\"\"\n if self.afd_config.role == \"attention\":\n return []\n return super().get_expert_mapping()\n\n def finalize_mega_moe_weights(self) -> None:\n \"\"\"MegaMoE is rejected before allocation, so no finalizer is needed.\"\"\"\n\n def finalize_mhc_broadcast_weights(self) -> None:\n \"\"\"Finalize only the Attention-owned first-layer broadcast matrix.\"\"\"\n if self.afd_config.role == \"ffn\":\n return\n if (\n not native.get_pp_group().is_first_rank\n or self.start_layer >= self.end_layer\n ):\n return" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I20 — DeepSeek-V4 的 mHC 和归一化只属于 Attention,权重分类、构造与限制检查必须同步\n\n- V4 的 `_checkpoint_weight_roles` 把层内所有非 `ffn` 路径都归给 Attention,显式名单里的 `hc_head_fn`/`hc_head_base`/`hc_head_scale`(带或不带 `model.` 前缀)也归 Attention。\n- 相应地,FFN 角色的 `AFDDeepseekV4DecoderLayer.__init__` 在创建 `attn_norm`/`ffn_norm`/`hc_*` 之前就 `return`。如果给 FFN 层新增不在 `ffn.` 下的参数,必须同时修改分类函数。\n- 不在 `layers.N.` 下的名字默认是 `_BOTH_ROLES`。只在 Attention 上构造的模型级参数必须加进显式名单。\n - `forward` 靠 `PPMissingLayer` 判断是否在 FFN 角色上。\n - `finalize_mhc_broadcast_weights` 在 FFN 角色上直接返回。\n - `get_expert_mapping` 在 Attention 角色上返回 `[]`。\n- `get_experts_layer_indices` 直接返回 `range(num_hidden_layers)`。这依赖两点:每一层都是 `RemoteDeepseekV4FFN`,以及 `AFDDeepseekV4Model.__init__` 拒绝 PP、SP MoE、EPLB、MegaMoE、`compute_gate_on_attention` 和非 `P2pNcclAFDConnector`。放宽其中任何一项(尤其是 PP)时,必须改成按实际层范围计算。\n\n\n" + }, + "id": "init-bad-deepen-AFD-I20", + "note": "kb init deepen: judge said fail; on main the text is absent", + "rule_id": "AFD-I20", + "source": "judge", + "stage": "deepen", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I27.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I27.json new file mode 100644 index 00000000..ab0d7aee --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I27.json @@ -0,0 +1,27 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/forward_context.py:L18-L34", + "text": "def get_afd_metadata_from_forward_context(\n forward_context: ForwardContext | None = None,\n) -> AFDForwardContextMetadata | None:\n \"\"\"Return AFD metadata from vLLM ``ForwardContext.additional_kwargs``.\n\n Model wrappers use this helper so AFD metadata stays outside vLLM's\n ``ForwardContext`` schema.\n \"\"\"\n\n if forward_context is None:\n forward_context = get_forward_context()\n\n additional_kwargs = forward_context.additional_kwargs or {}\n # Keep the type refinement static: torch.compile traces this helper and\n # cannot wrap the runtime ``types.UnionType`` created by typing.cast.\n metadata: AFDForwardContextMetadata | None = additional_kwargs.get(\"afd_metadata\")\n return metadata" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/forward_context.py:L37-L68", + "text": "@contextmanager\ndef use_afd_metadata_provider(\n installer: Callable[[ForwardContext], None],\n) -> Iterator[None]:\n \"\"\"Install AFD metadata as vLLM creates a forward context.\n\n ``installer`` receives each newly created ``ForwardContext`` exactly once.\n The native factory symbol is restored when the scope exits, including when\n context creation or installation raises.\n\n Native vLLM dummy runs call the model directly, bypassing\n ``AFDAttentionModelRunner._model_forward()``. Out-of-tree plugins cannot\n extend the ``set_forward_context()`` signature, so during dummy runs we\n temporarily wrap ``create_forward_context()`` and mutate\n ``additional_kwargs`` immediately after vLLM creates the context. Model code\n can then do a simple metadata read, which keeps ``torch.compile`` away from\n provider lookups.\n \"\"\"\n\n original_create = forward_context_module.create_forward_context\n\n @wraps(original_create)\n def create_forward_context_with_afd(*args: Any, **kwargs: Any) -> ForwardContext:\n forward_context = original_create(*args, **kwargs)\n installer(forward_context)\n return forward_context\n\n forward_context_module.create_forward_context = create_forward_context_with_afd\n try:\n yield\n finally:\n forward_context_module.create_forward_context = original_create" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I27 — `use_afd_metadata_provider` 的补丁必须能恢复,`get_afd_metadata_from_forward_context` 必须保持可被 torch.compile 追踪\n\n- 替换 `forward_context_module.create_forward_context` 之后,必须在 `finally` 里恢复原来的符号。如果异常路径漏掉恢复,之后创建的每个 context 都会执行 installer。\n- 这个补丁只对通过模块属性查找 `create_forward_context` 的调用生效,`from ... import create_forward_context` 这类早绑定会绕过它。升级 vLLM 时要核对 `set_forward_context` 是否仍然按模块全局名调用它。\n- installer 对每个新建的 context 只调用一次,并且只修改 `additional_kwargs`。键名 `\\\"afd_metadata\\\"` 被所有模型包装读取,不能改名。\n- `get_afd_metadata_from_forward_context` 会被编译图追踪。保持简单的 `dict.get` 加静态类型注解,不要引入 `typing.cast`、运行时联合类型检查或 provider 查找。\n\n\n" + }, + "id": "init-bad-deepen-AFD-I27", + "note": "kb init deepen: judge said fail; on main the text is absent", + "rule_id": "AFD-I27", + "source": "judge", + "stage": "deepen", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I31.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I31.json new file mode 100644 index 00000000..fd3d4a62 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I31.json @@ -0,0 +1,32 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/model_utils.py:L16-L46", + "text": "def has_afd_model_registration(model_config: ModelConfig) -> bool:\n \"\"\"Return whether the model resolves to a registered AFD implementation.\"\"\"\n\n return any(\n model_arch.removeprefix(\"AFD\") in _MODEL_REGISTRATIONS\n for model_arch in model_config.hf_config.architectures\n )\n\n\ndef get_afd_model_config(\n model_config: ModelConfig,\n *,\n device_type: Literal[\"cuda\", \"npu\"],\n) -> ModelConfig:\n \"\"\"Return a model config that resolves to an AFD model implementation.\"\"\"\n\n for model_arch in model_config.hf_config.architectures:\n if model_arch in _MODEL_REGISTRATIONS:\n if model_arch in _QWEN3_5_MODEL_REGISTRATIONS and device_type != \"cuda\":\n raise ValueError(\n \"AFD Qwen3.5/3.6 supports CUDA execution only; \"\n f\"got device_type={device_type!r}\",\n )\n # deepcopy preserves aliasing within the copied object graph, so\n # the pure-text identity hf_text_config is hf_config is retained\n # automatically. vLLM Ascend uses that identity to distinguish\n # text models from multimodal models.\n afd_model_config = deepcopy(model_config)\n afd_model_config.hf_config.architectures = [f\"AFD{model_arch}\"]\n return afd_model_config\n return model_config" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L871-L888", + "text": "class AFDDeepseekForCausalLM(AFDDeepseekV2ForCausalLM):\n pass\n\n\nclass AFDDeepseekV3ForCausalLM(AFDDeepseekV2ForCausalLM):\n pass\n\n\nclass AFDGlmMoeDsaForCausalLM(AFDDeepseekV2ForCausalLM):\n pass\n\n\n__all__ = [\n \"AFDDeepseekForCausalLM\",\n \"AFDDeepseekV2ForCausalLM\",\n \"AFDDeepseekV3ForCausalLM\",\n \"AFDGlmMoeDsaForCausalLM\",\n]" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v4.py:L476-L480", + "text": "class AFDDeepseekV4ForCausalLM(native.DeepseekV4ForCausalLM):\n \"\"\"DeepSeek-V4 causal LM exposing the GPU FFN-runner model contract.\"\"\"\n\n model_cls = AFDDeepseekV4Model\n afd_requires_input_ids = True" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I31 — `get_afd_model_config` 必须用 `deepcopy`,包装类名必须是 `AFD` 加 native 架构名\n\n- 必须保留 `deepcopy(model_config)`:\n - 它保留了 `hf_text_config is hf_config` 的对象同一性,vLLM-Ascend 靠这一点区分纯文本模型和多模态模型。\n - 换成浅拷贝会让 `architectures` 的改写泄漏到原始配置。\n- `get_afd_model_config` 只识别没有前缀的架构名,已经带 `AFD` 前缀的配置会原样返回;`has_afd_model_registration` 则用 `removeprefix(\\\"AFD\\\")`,两种写法都接受。修改时要保持这种幂等性和对称性。\n- 生成的 `AFD{model_arch}` 必须对应一个真实导出的类,例如 `AFDDeepseekV3ForCausalLM`、`AFDGlmMoeDsaForCausalLM` 这样的空子类。新增架构时,要同时:\n - 加入 `_MODEL_REGISTRATIONS`;\n - 提供同名的 `AFD` 类;\n - 把该类加入 `__all__`。\n\n\n" + }, + "id": "init-bad-deepen-AFD-I31", + "note": "kb init deepen: judge said fail; on main the text is absent", + "rule_id": "AFD-I31", + "source": "judge", + "stage": "deepen", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I32.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I32.json new file mode 100644 index 00000000..e85886eb --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I32.json @@ -0,0 +1,42 @@ +{ + "base": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L357-L362", + "text": " previous_is_graph_replaying = getattr(self, \"_afd_is_graph_replaying\", False)\n self._afd_is_graph_replaying = False\n try:\n return super().execute_model(scheduler_output, intermediate_tensors)\n finally:\n self._afd_is_graph_replaying = previous_is_graph_replaying" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L406-L433", + "text": " previous_metadata = self._afd_pending_metadata\n previous_is_graph_capturing = getattr(\n self,\n \"_afd_is_graph_capturing\",\n False,\n )\n self._afd_is_graph_capturing = is_graph_capturing\n try:\n with use_afd_metadata_provider(\n self.install_afd_metadata_on_forward_context,\n ):\n return super()._dummy_run(\n num_tokens,\n cudagraph_runtime_mode,\n force_attention,\n uniform_decode,\n allow_microbatching,\n skip_eplb,\n is_profile,\n create_mixed_batch,\n remove_lora,\n is_graph_capturing,\n num_active_loras,\n profile_seq_lens,\n )\n finally:\n self._afd_is_graph_capturing = previous_is_graph_capturing\n self._afd_pending_metadata = previous_metadata" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L464-L530", + "text": " # ### PATCH START: expose warmup state to the AFD control plane.\n previous_is_warmup = bool(self._is_warmup)\n try:\n self._is_warmup = True\n for _ in range(int(num_warmups)):\n self._dummy_run(\n desc.num_tokens,\n cudagraph_runtime_mode=CUDAGraphMode.NONE,\n force_attention=force_attention,\n uniform_decode=desc.uniform,\n allow_microbatching=allow_microbatching,\n skip_eplb=True,\n remove_lora=False,\n num_active_loras=desc.num_active_loras,\n profile_seq_lens=profile_seq_lens,\n )\n finally:\n self._is_warmup = previous_is_warmup\n # ### PATCH END: expose warmup state to the AFD control plane.\n\n # ### PATCH START: publish static AFD state before graph capture.\n previous_metadata = self._afd_pending_metadata\n previous_suppress_send = self._afd_suppress_metadata_send\n previous_is_graph_capturing = self._afd_is_graph_capturing\n try:\n # DP metadata transfer is a control-plane side effect. The original\n # AFD path sends it before formal CUDA graph capture so the capture\n # contains only replayable model/data-plane work.\n self._afd_is_graph_capturing = True\n if allow_microbatching:\n # The AFD-aware ubatch wrapper builds the exact padded ubatch\n # slices used by vLLM and sends per-stage DP metadata before it\n # enters torch.cuda.graph(...). Avoid sending a single-stage\n # capture payload here.\n self._afd_pending_metadata = None\n self._afd_suppress_metadata_send = False\n else:\n self._afd_pending_metadata = self.build_afd_metadata(\n None,\n int(desc.num_tokens),\n )\n self.send_dp_metadata(\n self.build_capture_dp_metadata(int(desc.num_tokens)),\n None,\n )\n self._afd_suppress_metadata_send = True\n with (\n profiler,\n torch.profiler.record_function(\n f\"capture_{desc.num_tokens}_{cudagraph_runtime_mode.name}\"\n ),\n ):\n self._dummy_run(\n desc.num_tokens,\n cudagraph_runtime_mode=cudagraph_runtime_mode,\n uniform_decode=desc.uniform,\n allow_microbatching=allow_microbatching,\n skip_eplb=True,\n remove_lora=False,\n num_active_loras=desc.num_active_loras,\n is_graph_capturing=True,\n profile_seq_lens=profile_seq_lens,\n )\n finally:\n self._afd_is_graph_capturing = previous_is_graph_capturing\n self._afd_suppress_metadata_send = previous_suppress_send\n self._afd_pending_metadata = previous_metadata" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L231-L256", + "text": " previous_metadata = runner._afd_pending_metadata\n previous_suppress_send = runner._afd_suppress_metadata_send\n previous_is_warmup = runner._is_warmup\n previous_is_graph_capturing = runner._afd_is_graph_capturing\n previous_is_graph_replaying = getattr(runner, \"_afd_is_graph_replaying\", False)\n runner._afd_is_graph_replaying = False\n\n replay_scope = (\n _use_afd_fullgraph_replay_hook(runner, real_tokens)\n if use_fullgraph_replay_hook\n else nullcontext()\n )\n try:\n with (\n replay_scope,\n use_afd_metadata_provider(\n runner.install_afd_metadata_on_forward_context,\n ),\n ):\n yield\n finally:\n runner._afd_pending_metadata = previous_metadata\n runner._afd_suppress_metadata_send = previous_suppress_send\n runner._is_warmup = previous_is_warmup\n runner._afd_is_graph_capturing = previous_is_graph_capturing\n runner._afd_is_graph_replaying = previous_is_graph_replaying" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L374-L391", + "text": " previous_metadata = self._afd_pending_metadata\n previous_suppress_send = self._afd_suppress_metadata_send\n previous_is_warmup = self._is_warmup\n previous_is_graph_capturing = self._afd_is_graph_capturing\n\n try:\n with _use_afd_capture_input_preparation(self, event_tracker):\n with use_afd_metadata_provider(\n self.install_afd_metadata_on_forward_context,\n ):\n result = super().capture_model()\n event_tracker.assert_complete()\n return result\n finally:\n self._afd_pending_metadata = previous_metadata\n self._afd_suppress_metadata_send = previous_suppress_send\n self._is_warmup = previous_is_warmup\n self._afd_is_graph_capturing = previous_is_graph_capturing" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n\n## AFD-I32 — AFD runner 状态标志只能在作用域内修改,旧值须在 `finally` 中恢复\n\n- `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying` 会被 `send_dp_metadata` 写进 `AFDControlPayload` 发给 FFN。`_afd_pending_metadata` 和 `_afd_suppress_metadata_send` 决定 `install_afd_metadata_on_forward_context` 复用哪个事务、要不要发送。任何一个值没被恢复,后续 eager 步骤都会被 FFN 当成 warmup、capture 或 replay,或者静默跳过控制面发送。\n- 修改 `_dummy_run`、`_warmup_and_capture`、`execute_model`(V1)、`capture_model`、`_use_afd_execution_context`(V2)时,逐个确认:被写入的字段在写入前先保存了旧值,并在 `finally` 中恢复。异常路径也必须恢复。\n- 新增 AFD 状态字段时,要把它同时加进上面所有保存/恢复点。除 `__init__` 外,不要在这些作用域之外直接给它们赋值。\n\n\n" + }, + "id": "init-bad-deepen-AFD-I32", + "note": "kb init deepen: judge said fail; on main the text is absent", + "rule_id": "AFD-I32", + "source": "judge", + "stage": "deepen", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I33.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I33.json new file mode 100644 index 00000000..4660e780 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I33.json @@ -0,0 +1,37 @@ +{ + "base": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L489-L509", + "text": " # DP metadata transfer is a control-plane side effect. The original\n # AFD path sends it before formal CUDA graph capture so the capture\n # contains only replayable model/data-plane work.\n self._afd_is_graph_capturing = True\n if allow_microbatching:\n # The AFD-aware ubatch wrapper builds the exact padded ubatch\n # slices used by vLLM and sends per-stage DP metadata before it\n # enters torch.cuda.graph(...). Avoid sending a single-stage\n # capture payload here.\n self._afd_pending_metadata = None\n self._afd_suppress_metadata_send = False\n else:\n self._afd_pending_metadata = self.build_afd_metadata(\n None,\n int(desc.num_tokens),\n )\n self.send_dp_metadata(\n self.build_capture_dp_metadata(int(desc.num_tokens)),\n None,\n )\n self._afd_suppress_metadata_send = True" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_metadata.py:L217-L224", + "text": " if getattr(self, \"_afd_suppress_metadata_send\", False):\n return\n dp_metadata = forward_context.dp_metadata\n ubatch_slices = forward_context.ubatch_slices\n padded_graph_tokens = _full_cudagraph_padded_tokens(forward_context)\n if padded_graph_tokens is not None and not ubatch_slices:\n dp_metadata = self.build_capture_dp_metadata(padded_graph_tokens)\n self.send_dp_metadata(dp_metadata, ubatch_slices)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L124-L136", + "text": " desc, is_warmup = event_tracker.consume(num_reqs, num_tokens)\n runner._is_warmup = is_warmup\n runner._afd_is_graph_capturing = not is_warmup\n runner._afd_pending_metadata = runner.build_afd_metadata(\n None,\n int(desc.num_tokens),\n )\n runner.send_dp_metadata(\n runner.build_capture_dp_metadata(int(desc.num_tokens)),\n None,\n )\n runner._afd_suppress_metadata_send = True\n return attention_state" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L190-L202", + "text": " padded_tokens = int(desc.num_tokens)\n metadata = runner.build_afd_metadata(None, real_tokens)\n metadata.tokens_lens = [padded_tokens]\n runner._afd_pending_metadata = metadata\n runner._afd_suppress_metadata_send = True\n runner._is_warmup = False\n runner._afd_is_graph_capturing = False\n runner._afd_is_graph_replaying = True\n runner.send_dp_metadata(\n runner.build_capture_dp_metadata(padded_tokens),\n None,\n )\n result = original_run_fullgraph(desc)" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n\n## AFD-I33 — DP 控制面 metadata 只能在 CUDA graph capture/replay 之外发送,发完置 `_afd_suppress_metadata_send`\n\n- V1 `_warmup_and_capture` 的非 ubatch 分支里,顺序必须是:先 `build_afd_metadata`,再 `send_dp_metadata(build_capture_dp_metadata(...), None)`,最后置 `_afd_suppress_metadata_send = True`。这样 capture 内的 `install_afd_metadata_on_forward_context` 只挂 sidecar,不发送。V2 的 `prepare_inputs_to_capture` 包装和 `run_fullgraph` hook 也都是先发送、再置 suppress。\n- `allow_microbatching` 的 capture 分支必须保持 `_afd_pending_metadata = None` 和 `_afd_suppress_metadata_send = False`。这个分支由 `AFDUBatchWrapper` 在进入 `torch.cuda.graph` 之前发送各 stage 的 payload。如果这里再补发一个单 stage payload,会重复发送,而且 stage 数也对不上。\n- 新增 graph 路径时,如果发送发生在 capture 里面,这次控制面副作用不会随 replay 重放,FFN 在 replay 时就收不到对应的控制。\n\n\n" + }, + "id": "init-bad-deepen-AFD-I33", + "note": "kb init deepen: judge said fail; on main the text is absent", + "rule_id": "AFD-I33", + "source": "judge", + "stage": "deepen", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I35.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I35.json new file mode 100644 index 00000000..94b957ec --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I35.json @@ -0,0 +1,32 @@ +{ + "base": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_metadata.py:L90-L117", + "text": " assert self.connector.control_plane is not None, (\n \"send_dp_metadata needs control plane driven connectors\"\n )\n\n if ubatch_slices and len(ubatch_slices) > 1:\n dp_metadata_list = {\n idx: metadata\n for idx, metadata in enumerate(\n build_ubatch_dp_metadata_list(self.vllm_config, ubatch_slices),\n )\n }\n else:\n dp_metadata = self._ensure_dp_metadata(dp_metadata)\n dp_metadata_list = {0: dp_metadata}\n is_warmup = self._is_warmup\n # Keep the V1 object.__new__ test seam and older graph lifecycle\n # callers compatible with runners created before this mixin existed.\n is_graph_capturing = getattr(self, \"_afd_is_graph_capturing\", False)\n is_graph_replaying = getattr(self, \"_afd_is_graph_replaying\", False)\n payload = AFDControlPayload(\n dp_metadata_list=dp_metadata_list,\n is_graph_capturing=is_graph_capturing,\n is_warmup=is_warmup,\n is_graph_replaying=is_graph_replaying,\n is_profile=self._afd_is_profile,\n )\n self.connector.control_plane.update_state_from_dp_metadata(payload)\n self.connector.control_plane.send_dp_metadata_list(payload)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_metadata.py:L119-L144", + "text": " def _ensure_dp_metadata(\n self,\n dp_metadata: DPMetadata | AFDDPMetadata | None,\n ) -> DPMetadata | AFDDPMetadata:\n if dp_metadata is not None:\n return dp_metadata\n\n dp_size = int(self.vllm_config.parallel_config.data_parallel_size)\n if dp_size != 1:\n raise RuntimeError(\"AFD expected vLLM DPMetadata for attention DP > 1\")\n\n if self._afd_pending_metadata is None:\n raise RuntimeError(\"AFD metadata is not available for DP metadata fallback\")\n if len(self._afd_pending_metadata.tokens_lens) != 1:\n raise RuntimeError(\"AFD DP=1 fallback only supports one stage\")\n\n num_tokens = int(self._afd_pending_metadata.tokens_lens[0])\n num_tokens_across_dp_cpu = torch.tensor(\n [num_tokens],\n dtype=torch.int32,\n device=\"cpu\",\n )\n return AFDDPMetadata(\n num_tokens_across_dp_cpu=num_tokens_across_dp_cpu,\n max_tokens_across_dp_cpu=torch.max(num_tokens_across_dp_cpu),\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_metadata.py:L146-L175", + "text": " def build_capture_dp_metadata(\n self,\n num_tokens: int,\n ) -> DPMetadata | AFDDPMetadata:\n \"\"\"Build, but do not send, uniform padded graph DP metadata.\n\n ``num_tokens`` is the padded CUDA graph token count executed on every DP\n rank during capture or replay. The returned metadata is suitable for\n :meth:`send_dp_metadata`; this method has no transaction, context, or\n connector side effects.\n \"\"\"\n\n dp_size = int(self.vllm_config.parallel_config.data_parallel_size)\n num_tokens_across_dp_cpu = torch.full(\n (dp_size,),\n int(num_tokens),\n dtype=torch.int32,\n device=\"cpu\",\n )\n if dp_size > 1:\n return DPMetadata.make(\n self.vllm_config.parallel_config,\n int(num_tokens),\n num_tokens_across_dp_cpu,\n )\n max_tokens_across_dp_cpu = torch.max(num_tokens_across_dp_cpu)\n return AFDDPMetadata(\n num_tokens_across_dp_cpu=num_tokens_across_dp_cpu,\n max_tokens_across_dp_cpu=max_tokens_across_dp_cpu,\n )" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n\n## AFD-I35 — `send_dp_metadata` 的前置条件和调用顺序\n\n- 同一个 payload 必须先 `update_state_from_dp_metadata(payload)`,再 `send_dp_metadata_list(payload)`。不要拆开,也不要调换。\n- `send_dp_metadata` 不分配事务。调用前必须已经用 `build_afd_metadata` stage 好 `_afd_pending_metadata`(capture 和 replay 路径都是先 build 再 send)。\n- 只有 DP=1 时才能传 `dp_metadata=None`,而且此时要求 pending metadata 只有单 stage,否则 `_ensure_dp_metadata` 会报错。DP>1 时必须传 vLLM `DPMetadata`。graph 路径用 `build_capture_dp_metadata` 构造各 rank 一致的 padded 计数。\n- 有多个 ubatch 时,payload 由 `build_ubatch_dp_metadata_list` 按 stage 生成,传入的 `dp_metadata` 会被忽略。\n\n\n" + }, + "id": "init-bad-deepen-AFD-I35", + "note": "kb init deepen: judge said fail; on main the text is absent", + "rule_id": "AFD-I35", + "source": "judge", + "stage": "deepen", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I41.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I41.json new file mode 100644 index 00000000..510673e3 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I41.json @@ -0,0 +1,32 @@ +{ + "base": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L46-L84", + "text": "class _AFDCaptureEventTracker:\n \"\"\"Consume each FULL descriptor once for warmup, then once for capture.\"\"\"\n\n def __init__(\n self,\n capture_descs: list[v2_cudagraph_utils.BatchExecutionDescriptor],\n ) -> None:\n self._expected_events = [\n (desc, is_warmup) for desc in capture_descs for is_warmup in (True, False)\n ]\n self._event_index = 0\n\n def consume(\n self,\n num_reqs: int,\n num_tokens: int,\n ) -> tuple[v2_cudagraph_utils.BatchExecutionDescriptor, bool]:\n if self._event_index >= len(self._expected_events):\n raise RuntimeError(\n \"AFD ModelRunnerV2 CUDA Graph observed extra capture input preparation\",\n )\n desc, is_warmup = self._expected_events[self._event_index]\n self._event_index += 1\n if num_reqs != desc.num_reqs or num_tokens != desc.num_tokens:\n raise RuntimeError(\n \"AFD ModelRunnerV2 CUDA Graph capture descriptor/order drift: \"\n f\"expected ({desc.num_reqs}, {desc.num_tokens}), got \"\n f\"({num_reqs}, {num_tokens})\",\n )\n return desc, is_warmup\n\n def assert_complete(self) -> None:\n if self._event_index != len(self._expected_events):\n raise RuntimeError(\n \"AFD ModelRunnerV2 CUDA Graph capture input-preparation \"\n \"call count drift: \"\n f\"expected {len(self._expected_events)}, got {self._event_index}\",\n )\n" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L113-L137", + "text": " # ### PATCH START: stage one exact AFD capture event.\n attention_state = original_prepare(\n num_reqs,\n num_tokens,\n model_state,\n input_buffers,\n block_tables,\n attn_groups,\n kv_cache_config,\n skip_attn,\n )\n desc, is_warmup = event_tracker.consume(num_reqs, num_tokens)\n runner._is_warmup = is_warmup\n runner._afd_is_graph_capturing = not is_warmup\n runner._afd_pending_metadata = runner.build_afd_metadata(\n None,\n int(desc.num_tokens),\n )\n runner.send_dp_metadata(\n runner.build_capture_dp_metadata(int(desc.num_tokens)),\n None,\n )\n runner._afd_suppress_metadata_send = True\n return attention_state\n # ### PATCH END: stage one exact AFD capture event." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L358-L391", + "text": " def capture_model(self) -> int:\n # ### PATCH START: publish AFD FULL graph warmup/capture control.\n manager = self.cudagraph_manager\n capture_descs = manager._capture_descs.get(CUDAGraphMode.FULL, [])\n if not capture_descs:\n raise RuntimeError(\n \"AFD ModelRunnerV2 CUDA Graph expected at least one FULL \"\n \"capture descriptor\",\n )\n for desc in capture_descs:\n if desc.cg_mode != CUDAGraphMode.FULL:\n raise RuntimeError(\n \"AFD ModelRunnerV2 CUDA Graph expected only FULL capture \"\n \"descriptors\",\n )\n event_tracker = _AFDCaptureEventTracker(capture_descs)\n previous_metadata = self._afd_pending_metadata\n previous_suppress_send = self._afd_suppress_metadata_send\n previous_is_warmup = self._is_warmup\n previous_is_graph_capturing = self._afd_is_graph_capturing\n\n try:\n with _use_afd_capture_input_preparation(self, event_tracker):\n with use_afd_metadata_provider(\n self.install_afd_metadata_on_forward_context,\n ):\n result = super().capture_model()\n event_tracker.assert_complete()\n return result\n finally:\n self._afd_pending_metadata = previous_metadata\n self._afd_suppress_metadata_send = previous_suppress_send\n self._is_warmup = previous_is_warmup\n self._afd_is_graph_capturing = previous_is_graph_capturing" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n\n## AFD-I41 — V2 capture 要求每个 FULL descriptor 恰好对应一次 warmup 加一次 capture\n\n- `_AFDCaptureEventTracker` 按 `capture_descs` 的顺序展开成 `(desc, True), (desc, False)` 序列。`consume` 逐个核对 `(num_reqs, num_tokens)`,`assert_complete` 核对总次数。上游改变 warmup 次数、descriptor 顺序或 `prepare_inputs_to_capture` 的调用方式时,这里会报错,需要一起调整。\n- `capture_model` 只接受 `CUDAGraphMode.FULL` descriptor,且至少要有一个。放开到其他模式前,必须先设计好对应的发送时机。\n- 包装后的 `prepare_inputs_to_capture` 必须先调用原函数,再 consume、发送,最后置 `_afd_suppress_metadata_send = True`。\n- `event_tracker.assert_complete()` 必须在 `super().capture_model()` 返回之后、原符号恢复之前执行。它负责发现补丁没有生效(一次都没被调用)的情况,不能删掉。\n\n\n" + }, + "id": "init-bad-deepen-AFD-I41", + "note": "kb init deepen: judge said fail; on main the text is absent", + "rule_id": "AFD-I41", + "source": "judge", + "stage": "deepen", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I44.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I44.json new file mode 100644 index 00000000..29415420 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I44.json @@ -0,0 +1,32 @@ +{ + "base": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L308-L321", + "text": " # DP metadata receive/update is a control-plane side effect and must\n # complete before CUDA graph capture starts.\n self.connector.control_plane.update_state_from_dp_metadata(\n _make_dp_metadata_payload(\n dp_metadata_list,\n is_graph_capturing=is_attn_graph_capturing,\n ),\n )\n with torch.cuda.graph(cudagraph, pool=self._graph_memory_pool):\n output = self._ffn_forward(\n dp_metadata_list=dp_metadata_list,\n is_graph_capturing=is_attn_graph_capturing,\n update_connector_state=False,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L351-L364", + "text": " if is_warmup:\n self.connector.control_plane.update_state_from_dp_metadata(\n _make_dp_metadata_payload(\n dp_metadata_list,\n is_graph_capturing=False,\n is_warmup=True,\n ),\n )\n self._ffn_forward(\n dp_metadata_list=dp_metadata_list,\n is_graph_capturing=False,\n is_warmup=True,\n update_connector_state=False,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L499-L516", + "text": " self.connector.control_plane.update_state_from_dp_metadata(\n _make_dp_metadata_payload(\n dp_metadata_list,\n is_graph_capturing=is_attn_graph_capturing,\n ),\n )\n logger.debug(\"AFD NPU FFN updated connector state for key=%s\", graph_key)\n with torch.npu.graph(graph, pool=self.graph_pool):\n logger.debug(\n \"AFD NPU FFN entered NPU graph context for key=%s\",\n graph_key,\n )\n output = self._ffn_forward(\n dp_metadata_list=dp_metadata_list,\n aclgraph_runtime_mode=aclgraph_runtime_mode,\n is_graph_capturing=is_attn_graph_capturing,\n update_connector_state=False,\n )" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n\n## AFD-I44 — 控制面 DP metadata 更新必须在 graph capture 之前完成,capture 内禁止再次更新\n\n- `update_state_from_dp_metadata` 是 host 侧控制面副作用(源码注释:receive/update 必须在 capture 开始前完成)。在 `GPUFFNModelRunner._dummy_run` 和 `AFDNPUFFNModelRunner._capture_graphs` 中,它都先于 `torch.cuda.graph(...)` / `torch.npu.graph(...)` 执行。\n- capture 内调用 `_ffn_forward` 时必须传 `update_connector_state=False`。GPU `capture_model` 的 `is_warmup` 分支同样先显式 update,再以 `update_connector_state=False` 调用。\n- 审查新增或改动的 capture/warmup 入口:每个 step 只能更新一次控制面状态,且不能把更新录进 graph。重复更新会让控制面状态与 Attention 侧不同步。\n\n\n" + }, + "id": "init-bad-deepen-AFD-I44", + "note": "kb init deepen: judge said fail; on main the text is absent", + "rule_id": "AFD-I44", + "source": "judge", + "stage": "deepen", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I49.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I49.json new file mode 100644 index 00000000..21d647f5 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I49.json @@ -0,0 +1,37 @@ +{ + "base": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L194-L199", + "text": " stage_ids = sorted(int(stage_idx) for stage_idx in dp_metadata_list) or [0]\n ffn_dp_metadata_list = {\n stage_idx: self._make_ffn_dp_metadata(dp_metadata_list[stage_idx])\n for stage_idx in stage_ids\n }\n recv_input_ids = getattr(self.model, \"afd_requires_input_ids\", False)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L223-L228", + "text": " if forward_context is not None:\n forward_context.dp_metadata = ffn_dp_metadata_list[\n metadata.stage_idx\n ]\n forward_context.additional_kwargs[\"afd_metadata\"] = metadata\n _set_moe_layer_index(forward_context, layer_idx)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L298-L301", + "text": " # A model whose router is keyed by token identity declares that the FFN\n # role needs the tokens' ids. Resolve it once per forward, outside the\n # layer loop, so a per-layer re-read cannot drift within a step.\n recv_input_ids = getattr(self.model, \"afd_requires_input_ids\", False)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L315-L351", + "text": " # DBO stages can have different token counts. Build a fresh\n # Ascend context for each stage so its MC2 padding mask matches\n # the hidden states received for that stage.\n dp_num_tokens_across_dp = _to_dp_level_token_counts(\n num_tokens_across_dp,\n dp_size=int(self.vllm_config.parallel_config.data_parallel_size),\n )\n # A model whose router is keyed by token identity needs the ids\n # of the tokens this rank computes on installed in the forward\n # context before the FFN compute runs. They arrive on the\n # transfer payload, so the receive must happen before the\n # context is built rather than inside it.\n payload = self.connector.recv_attn_output(\n ubatch_idx=stage_idx,\n layer_idx=layer_idx,\n max_num_tokens=self.max_num_tokens,\n recv_input_ids=recv_input_ids,\n )\n context = payload.context\n metadata = context.metadata\n states = context.states\n hidden_states = payload.hidden_states\n received_input_ids = payload.input_ids\n with ascend_forward_context(\n vllm_config=self.vllm_config,\n afd_metadata=afd_metadata,\n model_instance=self.model,\n num_tokens=num_tokens,\n num_tokens_across_dp=dp_num_tokens_across_dp,\n in_profile_run=is_profile,\n aclgraph_runtime_mode=aclgraph_runtime_mode,\n input_ids=received_input_ids,\n ) as forward_context:\n metadata.layer_idx = layer_idx\n metadata.stage_idx = stage_idx\n forward_context.dp_metadata = dp_metadata_list.get(stage_idx)\n forward_context.additional_kwargs[\"afd_metadata\"] = metadata" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n\n## AFD-I49 — forward context 按 stage 设置;NPU 必须先 recv 再构建 `ascend_forward_context`\n\n- DBO 各 stage 的 token 数可以不同。GPU 每次 recv 后都要把 `forward_context.dp_metadata` 设为 `ffn_dp_metadata_list[stage_idx]`,并更新 `afd_metadata` 和 `_set_moe_layer_index`。NPU 为每个 (layer, stage) 新建 `ascend_forward_context`,让 MC2 padding mask 与该 stage 的 hidden states 匹配。不要把 context 构建或 dp_metadata 赋值提到 stage 循环外。\n- NPU 构建 context 时就要通过 `input_ids=received_input_ids` 安装 token id,所以 `recv_attn_output` 必须在 `with ascend_forward_context(...)` 之前完成。\n- `recv_input_ids` 在层循环外只解析一次(两个平台都是),避免一个 step 内前后读到的值不一致。不要移到循环内逐层读取。\n\n\n" + }, + "id": "init-bad-deepen-AFD-I49", + "note": "kb init deepen: judge said fail; on main the text is absent", + "rule_id": "AFD-I49", + "source": "judge", + "stage": "deepen", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I52.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I52.json new file mode 100644 index 00000000..11c0fdd5 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I52.json @@ -0,0 +1,42 @@ +{ + "base": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_worker.py:L159-L171", + "text": " def ffn_worker_loop() -> None:\n try:\n self._run_ffn_server_loop()\n except Exception as exc:\n self._ffn_loop_error = exc\n logger.exception(\"AFD FFN worker loop failed\")\n\n self._ffn_thread = threading.Thread(\n target=ffn_worker_loop,\n name=\"afd-ffn-worker-loop\",\n daemon=True,\n )\n self._ffn_thread.start()" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_worker.py:L178-L179", + "text": " if self.device.type == \"cuda\":\n torch.cuda.set_device(self.device)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_worker.py:L211-L218", + "text": " if self.device.type == \"cuda\":\n torch.cuda.synchronize()\n\n def raise_ffn_loop_error_if_any(self) -> None:\n error = self._ffn_loop_error\n if error is not None:\n self._ffn_loop_error = None\n raise RuntimeError(\"AFD FFN worker loop failed\") from error" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_worker.py:L164-L174", + "text": " def _run_ffn_server_loop(self) -> None:\n event = self._ffn_shutdown_event\n if event is None:\n return\n\n torch.npu.set_device(self.device)\n while not event.is_set():\n if self.model_runner.connector.control_plane is None:\n self.model_runner.execute_connector_driven_step()\n torch.npu.synchronize()\n continue" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_worker.py:L190-L196", + "text": " torch.npu.synchronize()\n\n def raise_ffn_loop_error_if_any(self) -> None:\n error = self._ffn_loop_error\n if error is not None:\n self._ffn_loop_error = None\n raise RuntimeError(\"AFD NPU FFN worker loop failed\") from error" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n\n## AFD-I52 — FFN daemon 线程必须自己设置设备,每步末尾 synchronize,异常经 `_ffn_loop_error` 上抛\n\n- FFN 循环跑在独立的 daemon 线程(`afd-ffn-worker-loop` / `afd-npu-ffn-worker-loop`)中,当前设备是线程局部状态。`_run_ffn_server_loop` 开头的 `torch.cuda.set_device` / `torch.npu.set_device` 不能删掉,也不能移到线程外执行。\n- 当前循环以每步末尾的 `synchronize()` 作为 step 边界,然后才读取下一次 `recv_dp_metadata_list`。去掉它或改成异步,需要单独证明安全。\n- 线程内的异常只记入 `_ffn_loop_error`,由 `raise_ffn_loop_error_if_any` 在 `start_ffn_server_loop` / `stop_ffn_server_loop` 中抛出。新增的线程入口或循环分支必须走同一捕获路径,否则 daemon 线程会把错误吞掉。\n\n\n" + }, + "id": "init-bad-deepen-AFD-I52", + "note": "kb init deepen: judge said fail; on main the text is absent", + "rule_id": "AFD-I52", + "source": "judge", + "stage": "deepen", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I53.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I53.json new file mode 100644 index 00000000..ac4703d8 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I53.json @@ -0,0 +1,42 @@ +{ + "base": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_worker.py:L123-L135", + "text": " def ffn_worker_loop() -> None:\n try:\n self._run_ffn_server_loop()\n except Exception as exc:\n shutdown_event = self._ffn_shutdown_event\n if shutdown_event is not None and shutdown_event.is_set():\n logger.debug(\n \"AFD NPU FFN receive loop stopped during shutdown\",\n exc_info=True,\n )\n return\n self._ffn_loop_error = exc\n logger.exception(\"AFD NPU FFN worker loop failed\")" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_worker.py:L198-L222", + "text": " def stop_ffn_server_loop(self) -> None:\n event = self._ffn_shutdown_event\n if event is not None:\n event.set()\n\n # CAM recv blocks in the connector operator. Release the communicator\n # first so the daemon can observe the shutdown event, then wait for it\n # before the parent runner releases model tensors.\n self.model_runner.connector.close()\n thread = self._ffn_thread\n if thread is not None:\n thread.join(timeout=FFN_SHUTDOWN_TIMEOUT_SECONDS)\n if thread.is_alive():\n raise RuntimeError(\n \"AFD NPU FFN worker loop did not stop after connector close\",\n )\n self._ffn_thread = None\n self._ffn_shutdown_event = None\n self.raise_ffn_loop_error_if_any()\n\n def shutdown(self) -> None:\n # Stop the connector-driven daemon before NPUWorker releases the model\n # runner and its tensors.\n self.stop_ffn_server_loop()\n super().shutdown()" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_worker.py:L220-L236", + "text": " def stop_ffn_server_loop(self) -> None:\n event = self._ffn_shutdown_event\n if event is not None:\n event.set()\n try:\n self.model_runner.shutdown()\n finally:\n thread = self._ffn_thread\n if thread is not None:\n thread.join(timeout=5)\n self._ffn_thread = None\n self._ffn_shutdown_event = None\n self.raise_ffn_loop_error_if_any()\n\n def shutdown(self) -> None:\n self.stop_ffn_server_loop()\n super().shutdown()" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L411-L424", + "text": " def shutdown(self) -> None:\n # ### PATCH START: release native-equivalent and AFD-owned GPU state.\n stop_afd_gpu_profiler(self.prof)\n try:\n for graph_info in self._cuda_graphs.values():\n graph_info[\"graph\"].reset()\n self._cuda_graphs.clear()\n self._graph_memory_pool = None\n self.vllm_config.compilation_config.static_forward_context.clear()\n self.model = None\n _ROPE_DICT.clear()\n reset_workspace_manager()\n finally:\n self.connector.close()" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L530-L537", + "text": " def shutdown(self) -> None:\n if self._is_shutdown:\n return\n stop_afd_npu_profiler(self.prof)\n if self.connector.is_initialized:\n self.connector.close()\n super().shutdown()\n self._is_shutdown = True" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n\n## AFD-I53 — 停止顺序固定为 event → 关闭 connector → join → 父类 shutdown,runner `shutdown` 须可重复调用\n\n- NPU `stop_ffn_server_loop` 的顺序是:先 `event.set()`;再 `connector.close()`,解除 CAM recv 的阻塞;再 `join(timeout=FFN_SHUTDOWN_TIMEOUT_SECONDS)`,线程仍存活就报错。之后 `shutdown` 才调用 `super().shutdown()` 释放 runner 和张量。\n- NPU `ffn_worker_loop` 只在 shutdown event 已置位时,才把异常降级为 debug 日志。如果把 `connector.close()` 挪到 `event.set()` 之前,close 触发的 recv 异常会被记为 `_ffn_loop_error`,并在 stop 时抛出。\n- GPU 通过 `model_runner.shutdown()` 在 `finally` 中执行 `connector.close()`,之后才 join。runner `shutdown` 新增的清理步骤要放进 `try`,并保持 `connector.close()` 留在 `finally`。\n- runner `shutdown` 可能被调用不止一次,NPU 用 `_is_shutdown` 和 `connector.is_initialized` 防护。新增步骤必须能容忍第二次调用。\n\n\n" + }, + "id": "init-bad-deepen-AFD-I53", + "note": "kb init deepen: judge said fail; on main the text is absent", + "rule_id": "AFD-I53", + "source": "judge", + "stage": "deepen", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I56.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I56.json new file mode 100644 index 00000000..b084f05a --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I56.json @@ -0,0 +1,37 @@ +{ + "base": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/async_cam.py:L843-L875", + "text": " The world is Attention-first: Attention role rank ``i`` maps to world rank\n ``i`` and FFN role rank ``j`` maps to\n ``num_attention_ranks + j``. Routed experts are evenly distributed across FFN\n ranks, so the routed-expert count must divide by the FFN rank count.\n \"\"\"\n attn_size = afd_config.num_attention_ranks\n ffn_size = afd_config.num_ffn_ranks\n if attn_size <= 0 or ffn_size <= 0:\n raise ValueError(\"AFD async topology sizes must be positive\")\n if role_rank < 0:\n raise ValueError(f\"AFD async role rank must be non-negative, got {role_rank}\")\n\n if afd_config.role == \"attention\":\n if role_rank >= attn_size:\n raise ValueError(\n \"Attention role rank must be within attention size \"\n f\"(rank={role_rank}, size={attn_size})\",\n )\n world_rank = role_rank\n elif afd_config.role == \"ffn\":\n if role_rank >= ffn_size:\n raise ValueError(\n \"FFN role rank must be within FFN size \"\n f\"(rank={role_rank}, size={ffn_size})\",\n )\n world_rank = attn_size + role_rank\n else:\n raise ValueError(f\"unknown AFD role {afd_config.role!r}\")\n\n expert_count = num_routed_experts if num_routed_experts is not None else ffn_size\n if expert_count % ffn_size:\n raise ValueError(\"Async CAM requires routed experts divisible by FFN ranks\")\n expert_per_rank = expert_count // ffn_size" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L851-L866", + "text": " if afd_config.role == \"attention\":\n if role_rank >= attention_size:\n raise ValueError(\n \"Attention role rank must be within attention size \"\n f\"(rank={role_rank}, size={attention_size})\",\n )\n world_rank = ffn_size + role_rank\n p2p_rank = role_rank + min(ffn_size, attention_size)\n elif afd_config.role == \"ffn\":\n if role_rank >= ffn_size:\n raise ValueError(\n \"FFN role rank must be within FFN size \"\n f\"(rank={role_rank}, size={ffn_size})\",\n )\n world_rank = role_rank\n p2p_rank = role_rank" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L443-L454", + "text": " if self.afd_config.role == \"ffn\":\n self.ffn_pg = init_afd_process_group(\n backend=\"hccl\",\n init_method=f\"tcp://{self.afd_config.host}:{self.afd_config.port}\",\n world_size=self.ffn_size,\n rank=self.world_rank,\n group_name=\"afd_moe\",\n timeout=timedelta(minutes=30),\n pg_options=create_hccl_process_group_options(\n self.hccl_buffer_size_mb,\n ),\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L216-L224", + "text": " @property\n def participates_in_p2p_group(self) -> bool:\n \"\"\"Return whether this process joins the Gloo DP-metadata group.\"\"\"\n return self.world_rank < self.ffn_size or self.is_attn_top_min_size_rank\n\n @property\n def is_attn_top_min_size_rank(self) -> bool:\n \"\"\"Return whether this is an Attention metadata-sender rank.\"\"\"\n return self.ffn_size <= self.world_rank < self.ffn_size + self.min_size" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n\n## AFD-I56 — 两个 NPU connector 的 world rank 布局相反,不能互相复用,也不能只改一侧\n\n- `afd_plugin/connectors/npu/async_cam.py::build_async_topology` 把 Attention 放在前面:Attention 的 `world_rank = role_rank`,FFN 的 `world_rank = attn_size + role_rank`。\n- `afd_plugin/connectors/npu/camp2p.py::build_camp2p_topology` 把 FFN 放在前面:FFN 的 `world_rank = role_rank`,Attention 的 `world_rank = ffn_size + role_rank`。\n- review 时确认改动没有把一套公式搬到另一个 connector,也没有把两者抽成共享 helper。\n- `world_rank`、`attn_size`、`ffn_size` 会原样传给 `torch.ops.afd_ascend.*`,算子按这个布局找对端 rank。只改 Python 侧公式而不改算子,会静默发到错误的 rank。\n- CAMP2P 有两处代码依赖 FFN 占据 `[0, ffn_size)`:\n - `afd_moe` 组以 `world_size=self.ffn_size, rank=self.world_rank` 创建;\n - `_CAMP2PTopology.participates_in_p2p_group` 用 `world_rank < ffn_size` 判断 FFN rank。\n- async CAM 的 `expert_per_rank = num_routed_experts // ffn_size` 会传给全部四个 CAM 算子。整除校验不能删除,否则专家分配会静默错位。\n\n\n" + }, + "id": "init-bad-deepen-AFD-I56", + "note": "kb init deepen: judge said fail; on main the text is absent", + "rule_id": "AFD-I56", + "source": "judge", + "stage": "deepen", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I57.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I57.json new file mode 100644 index 00000000..18540cae --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I57.json @@ -0,0 +1,32 @@ +{ + "base": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/async_cam.py:L468-L499", + "text": " self._require_initialized()\n metadata = context.metadata\n if not metadata.validate_tensor_shape(tuple(hidden_states.shape)):\n raise ValueError(\n f\"hidden_states shape {hidden_states.shape!r} does not match \"\n f\"AFD async metadata token count {metadata.total_tokens}\",\n )\n states = AFDAsyncTransferState(\n batch_size=metadata.total_tokens,\n hidden_size=self.hidden_size,\n topk=self.topk,\n layer_idx=metadata.layer_idx,\n )\n context.states = states\n topk_ids = kwargs.get(\"topk_ids\")\n topk_weights = kwargs.get(\"topk_weights\")\n if topk_ids is None or topk_weights is None:\n raise RuntimeError(\n \"CAMAsyncAFDConnector send_attn_output requires topk_ids/topk_weights\",\n )\n topk_ids = topk_ids.to(dtype=torch.int32).contiguous()\n topk_weights = topk_weights.to(dtype=torch.float32).contiguous()\n _validate_topk_payload(\n topk_ids,\n topk_weights,\n batch_size=states.batch_size,\n topk=states.topk,\n )\n if states.batch_size > self.max_num_batched_tokens // self.tp_size:\n raise ValueError(\n \"Async CAM batch exceeds max_num_batched_tokens / TP capacity\"\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/async_cam.py:L523-L546", + "text": " torch.ops.afd_ascend.afd_async_dispatch_send(\n hidden_states,\n topk_ids,\n self.comm_args,\n self.comm_id,\n self.max_num_batched_tokens,\n states.batch_size,\n states.hidden_size,\n states.topk,\n self.ffn_size,\n self.attn_size,\n self.expert_per_rank,\n self.world_rank,\n self.topology.world_size,\n states.layer_idx,\n self.tp_size,\n self.dynamic_quant,\n self.group_name,\n )\n self._pending_attention_payloads.setdefault(\n context.metadata.stage_idx, []\n ).append(\n (context, topk_ids, topk_weights),\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/async_cam.py:L579-L609", + "text": " context = kwargs.get(\"context\")\n topk_ids = kwargs.get(\"topk_ids\")\n topk_weights = kwargs.get(\"topk_weights\")\n if context is None or topk_ids is None or topk_weights is None:\n payloads = self._pending_attention_payloads.get(ubatch_idx)\n if not payloads:\n raise RuntimeError(\n \"CAMAsyncAFDConnector recv_ffn_output is missing pending \"\n \"Attention metadata\",\n )\n (\n pending_context,\n pending_topk_ids,\n pending_topk_weights,\n ) = payloads.pop(0)\n if not payloads:\n self._pending_attention_payloads.pop(ubatch_idx, None)\n if context is None:\n context = pending_context\n if topk_ids is None:\n topk_ids = pending_topk_ids\n if topk_weights is None:\n topk_weights = pending_topk_weights\n\n states = _require_async_transfer_state(context)\n _validate_topk_payload(\n topk_ids,\n topk_weights,\n batch_size=states.batch_size,\n topk=states.topk,\n )" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n\n## AFD-I57 — async CAM:`send_attn_output` 的入队和 `recv_ffn_output` 的出队必须一一配对\n\n- `send_attn_output` 以 `context.metadata.stage_idx` 为 key,把 `(context, topk_ids, topk_weights)` 追加到 `_pending_attention_payloads`。`recv_ffn_output` 以 `ubatch_idx` 为 key 按 FIFO 弹出。\n- 因此调用方两侧必须用同一个 stage 索引,而且同一 stage 内 recv 的顺序要和 send 一致。\n- 只有 `context`、`topk_ids`、`topk_weights` 至少缺一个时才会弹出队列。如果三者都显式传入,队列项会留下,下一次 recv 会拿到过期的路由数据。新调用方要么三者都不传,要么保证对应队列项被消费掉。\n- 显式传入的 `topk_ids`/`topk_weights` 不会经过 send 侧的 `int32`/`float32` 转换和 `.contiguous()`,调用方要自己保证 dtype 和连续性。\n- 显式传入的 `context` 必须带有 send 写入的 `AFDAsyncTransferState`。\n- 形状、top-k 和 `max_num_batched_tokens // tp_size` 容量校验必须放在 `afd_async_dispatch_send` 之前。\n- 算子发出之后,到入队之前不能再有提前返回或抛错。否则 FFN 已经收到数据,而 Attention 侧没有对应的 combine-recv 元数据。\n\n\n" + }, + "id": "init-bad-deepen-AFD-I57", + "note": "kb init deepen: judge said fail; on main the text is absent", + "rule_id": "AFD-I57", + "source": "judge", + "stage": "deepen", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I60.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I60.json new file mode 100644 index 00000000..302ab028 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I60.json @@ -0,0 +1,27 @@ +{ + "base": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/async_cam.py:L746-L765", + "text": " _log_cam_op_values(\n \"async_combine_send\",\n \"inputs\",\n ffn_output=ffn_output,\n comm_args=self.comm_args,\n token_nums_rankid_layeridx=token_nums_rankid_layeridx,\n metadata_values=not self.layered_gmm_enabled,\n comm_id=self.comm_id,\n max_seq_len=self.max_num_batched_tokens,\n batch_size=states.batch_size,\n hidden_size=states.hidden_size,\n topk=states.topk,\n ffn_size=self.ffn_size,\n attn_size=self.attn_size,\n expert_per_rank=self.expert_per_rank,\n rank=self.world_rank,\n world_size=self.topology.world_size,\n tp_size=self.tp_size,\n group_name=self.group_name,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/async_cam.py:L800-L832", + "text": "_CAM_LOG_SKIPPED_ARGS = frozenset({\"comm_args\", \"comm_id\", \"group_name\"})\n_CAM_OP_IO_LOG_ENV = \"AFD_CAM_OP_IO_LOG\"\n\n\ndef _log_cam_op_values(\n op_name: str, label: str, *, metadata_values: bool = True, **kwargs: object\n) -> None:\n if os.environ.get(_CAM_OP_IO_LOG_ENV, \"\").lower() not in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n }:\n return\n lines: list[str] = []\n for name, value in kwargs.items():\n if name in _CAM_LOG_SKIPPED_ARGS:\n continue\n if isinstance(value, Tensor):\n description = (\n f\"Tensor(dtype={value.dtype}, shape={tuple(value.shape)}, \"\n f\"device={value.device})\"\n )\n if metadata_values and name == \"token_nums_rankid_layeridx\":\n try:\n first5: object = value.detach().flatten()[:5].cpu().tolist()\n except Exception as exc: # pragma: no cover - defensive logging helper\n first5 = f\"\"\n description = f\"{description}, first5={first5!r}\"\n else:\n description = repr(value)\n lines.append(f\" {name}={description}\")\n logger.warning(\"AFD CAM %s %s:\\n%s\", op_name, label, \"\\n\".join(lines))" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n\n## AFD-I60 — `_log_cam_op_values` 在日志关闭时必须零开销,不能引入设备同步\n\n- 函数第一步检查 `AFD_CAM_OP_IO_LOG`,关闭时直接返回。新增逻辑不能放到这个检查之前。\n- 张量默认只打印 dtype、shape 和 device。唯一的 `.cpu()` 读值只针对 `token_nums_rankid_layeridx`,并且受 `metadata_values` 控制。\n- `send_ffn_output` 在 `layered_gmm_enabled` 时传 `metadata_values=False`,用来避免同步。新增的读值要走同一个开关。\n- 调用参数在进入函数之前就已经求值。调用处不能传 `.item()`、`.cpu()`、`.tolist()` 这类表达式,否则即使日志关闭也会触发同步。\n\n\n" + }, + "id": "init-bad-deepen-AFD-I60", + "note": "kb init deepen: judge said fail; on main the text is absent", + "rule_id": "AFD-I60", + "source": "judge", + "stage": "deepen", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I61.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I61.json new file mode 100644 index 00000000..6b90646b --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I61.json @@ -0,0 +1,37 @@ +{ + "base": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/async_cam.py:L286-L329", + "text": " def init_afd_connector(self) -> None:\n \"\"\"Collectively initialize the CAM HCCL world and operator buffers.\n\n All Attention and FFN ranks must call this method with identical\n rendezvous and topology settings. Missing/duplicate ranks or mismatched\n world sizes fail or time out during the 30-minute rendezvous.\n \"\"\"\n if self._initialized:\n return\n\n ensure_cam_async_ops_available()\n self.cam_pg = init_afd_process_group(\n backend=\"hccl\",\n init_method=f\"tcp://{self.afd_config.host}:{self.afd_config.port}\",\n world_size=self.topology.world_size,\n rank=self.world_rank,\n group_name=AFD_ASYNC_CAM_GROUP_NAME,\n timeout=timedelta(minutes=30),\n pg_options=create_hccl_process_group_options(\n self.hccl_buffer_size_mb,\n ),\n )\n backend = self.cam_pg._get_backend(torch.device(\"npu\"))\n self.group_name = str(backend.get_hccl_comm_name(self.world_rank))\n device = f\"npu:{self.local_rank}\"\n self.comm_args = torch.empty((1,), dtype=torch.float16, device=device)\n self._placeholder = torch.empty(\n (1,),\n dtype=self.activation_dtype,\n device=device,\n )\n self._initialized = True\n\n def close(self) -> None:\n \"\"\"Destroy the HCCL process group and clear pending transfer states.\"\"\"\n if self.cam_pg is not None:\n import torch.distributed as dist\n\n dist.destroy_process_group(self.cam_pg)\n self.cam_pg = None\n self.comm_args = None\n self._placeholder = None\n self._pending_attention_payloads.clear()\n self._initialized = False" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/async_cam.py:L783-L785", + "text": " def _require_initialized(self) -> None:\n if not self._initialized:\n raise RuntimeError(\"CAMAsyncAFDConnector is not initialized\")" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L407-L408", + "text": " if self._initialized:\n return" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L470-L498", + "text": " self._initialized = True\n\n def close(self) -> None:\n \"\"\"Close all communication groups created by this connector.\n\n The method also clears saved HCCL group names and marks the connector as\n uninitialized. It is safe to initialize the connector again afterward.\n \"\"\"\n groups = [self.p2p_pg, self.ffn_pg, *self.afd_pg_list]\n if self.afd_pg is not None and not self.afd_pg_list:\n groups.append(self.afd_pg)\n destroyed_group_ids: set[int] = set()\n for group in groups:\n if group is not None:\n group_id = id(group)\n if group_id in destroyed_group_ids:\n continue\n destroyed_group_ids.add(group_id)\n dist.destroy_process_group(group)\n self.p2p_pg = None\n self.ffn_pg = None\n self.afd_pg = None\n self.afd_pg_list = []\n self.hccl_comm_name = \"\"\n self.hccl_comm_name2 = \"\"\n self.hccl_comm_name3 = \"\"\n self.hccl_comm_name1 = \"\"\n self.hccl_comm_name_list = []\n self._initialized = False" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n\n## AFD-I61 — `init_afd_connector` 与 `close` 必须对称,数据面方法必须检查是否已初始化\n\n- 两个 connector 都在所有组和 buffer 创建完成后才设置 `_initialized = True`,并在函数开头据此提前返回。新增资源必须在置位之前创建,否则部分失败后连接器会被当作已初始化。\n- 初始化之前,`group_name`/`hccl_comm_name*` 是空串,`comm_args`/`_placeholder` 是 `None`。\n- 因此每个新增的数据面方法都要先检查初始化状态:async 调用 `_require_initialized()`,CAMP2P 检查 `self._initialized`。不能把空 group 名传给算子。\n- `close()` 必须销毁 init 创建的每个 process group,并重置所有派生字段,保证之后可以再次 init。新增的组要加入 `close()` 里的 `groups` 列表。\n- CAMP2P 的 `close()` 按 `id()` 去重,因为 `afd_pg` 是 `afd_pg_list[0]` 的别名。\n- async 的 `close()` 必须清空 `_pending_attention_payloads`,否则重连后 `recv_ffn_output` 会弹出上一次会话的路由数据。\n\n\n" + }, + "id": "init-bad-deepen-AFD-I61", + "note": "kb init deepen: judge said fail; on main the text is absent", + "rule_id": "AFD-I61", + "source": "judge", + "stage": "deepen", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I67.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I67.json new file mode 100644 index 00000000..267d087d --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-deepen-AFD-I67.json @@ -0,0 +1,42 @@ +{ + "base": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L655-L701", + "text": " batch_size = _num_tokens_for_ffn_rank(\n self.dp_metadata_list,\n ubatch_idx,\n ffn_rank=self.role_rank,\n attention_size=self.attn_size,\n ffn_size=self.ffn_size,\n fallback=max_num_tokens,\n )\n metadata = AFDTransferMetadata.create_ffn_metadata(\n layer_idx=layer_idx,\n stage_idx=ubatch_idx,\n seq_lens=[batch_size],\n )\n custom_states = CAMP2PTransferState(\n aiv_num=self.aiv_num,\n batch_size=batch_size,\n h=self.hidden_size,\n k=self.num_experts_per_tok,\n )\n context = AFDTransferContext(\n metadata=metadata,\n states=custom_states,\n )\n\n group_ep = _get_group_ep(\n ubatch_idx,\n self.hccl_comm_name,\n self.hccl_comm_name2,\n self.hccl_comm_name3,\n )\n outputs = torch.ops.afd_ascend.a2e(\n torch.tensor([], dtype=torch.bfloat16, device=\"npu\"),\n torch.tensor([], dtype=torch.int32, device=\"npu\"),\n torch.tensor([], dtype=torch.float32, device=\"npu\"),\n custom_states.batch_size,\n custom_states.h,\n custom_states.k,\n self.ffn_size,\n self.attn_size,\n self.world_rank,\n group_ep,\n custom_states.aiv_num,\n compute_gate_mode,\n )\n custom_states.atten_batch_size = outputs[3]\n custom_states.x_active_mask = outputs[4]\n custom_states.cam_p2p_ep_name = self.hccl_comm_name1" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L742-L763", + "text": " states = cast(CAMP2PTransferState, context.states)\n if states.atten_batch_size is None:\n raise RuntimeError(\"CAMP2P FFN side is missing A2E atten_batch_size\")\n ubatch_idx = int(kwargs.get(\"ubatch_idx\", context.metadata.stage_idx))\n group_ep = _get_group_ep(\n ubatch_idx,\n self.hccl_comm_name,\n self.hccl_comm_name2,\n self.hccl_comm_name3,\n )\n torch.ops.afd_ascend.e2a(\n ffn_output,\n states.atten_batch_size,\n states.batch_size,\n states.h,\n states.k,\n self.ffn_size,\n self.attn_size,\n self.world_rank,\n group_ep,\n states.aiv_num,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L780-L787", + "text": " def update_state_from_dp_metadata(\n self,\n payload: AFDControlPayload,\n ) -> None:\n connector = self.connector\n connector.dp_metadata_list = payload.dp_metadata_list\n connector.is_graph_capturing = payload.is_graph_capturing\n connector.is_warmup = payload.is_warmup" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L917-L936", + "text": " dp_metadata = dp_metadata_list[stage_idx]\n if dp_metadata is None:\n return max(1, fallback)\n token_counts = dp_metadata.num_tokens_across_dp_cpu\n counts = token_counts.flatten().tolist()\n # Expand DP-level counts to AFD-level when TP > 1.\n # num_tokens_across_dp_cpu has dp_size entries, but attention_size\n # = num_attention_ranks includes TP workers. Each DP rank's count\n # is replicated tp_size times.\n if len(counts) < attention_size and attention_size % len(counts) == 0:\n tp_size = attention_size // len(counts)\n counts = [counts[i // tp_size] for i in range(attention_size)]\n if len(counts) < attention_size:\n return max(1, fallback)\n if attention_size >= ffn_size and attention_size % ffn_size == 0:\n group_size = attention_size // ffn_size\n start_idx = ffn_rank * group_size\n end_idx = start_idx + group_size\n return max(1, sum(counts[start_idx:end_idx]))\n return max(1, fallback)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L1010-L1010", + "text": " transfer_state.atten_batch_size = outputs[3]" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n\n## AFD-I67 — CAMP2P FFN 侧:token 数来自已应用的 DP 元数据,`send_ffn_output` 必须使用同一次 recv 的 context\n\n- `_num_tokens_for_ffn_rank` 直接索引 `dp_metadata_list[stage_idx]`。缺少这个 key 会抛 KeyError,只有值为显式 `None` 时才回退到 `max_num_tokens`。\n- 因此每个 stage 在 `recv_attn_output` 之前,都要先经过 `control_plane.update_state_from_dp_metadata`。\n- 一个 FFN rank 的 token 数是连续 `attention_size // ffn_size` 个 Attention rank 的计数之和,且至少为 1。DP 计数会先按 TP 复制扩展到 `attention_size` 个条目。\n- 这个映射必须和 A2E 算子的 Attention→FFN 分配一致,否则 batch 大小会与实际收到的数据不符。\n- `send_ffn_output` 依赖 recv 阶段写入的 `states.atten_batch_size`(A2E 的 `outputs[3]`),所以必须传入 `recv_attn_output` 返回的 `context`。通过 kwargs 覆盖的 `ubatch_idx` 也必须选中和 recv 相同的组。\n- A2E 输出下标 `[0]` hidden、`[1]` ids、`[3]` atten_batch_size、`[4]` x_active_mask 在 `recv_attn_output` 和 `send_attn_output_impl` 两处使用。算子输出布局变化时两处要一起改。\n\n\n" + }, + "id": "init-bad-deepen-AFD-I67", + "note": "kb init deepen: judge said fail; on main the text is absent", + "rule_id": "AFD-I67", + "source": "judge", + "stage": "deepen", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-skeleton-AFD-I15.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-skeleton-AFD-I15.json new file mode 100644 index 00000000..afb99cd4 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-skeleton-AFD-I15.json @@ -0,0 +1,57 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L82-L82", + "text": "| CUDA DBO Attention metadata workaround | [`attention_model_runner.py`](../../../afd_plugin/v1/worker/attention_model_runner.py) | restoration and error-path coverage in [`test_attention_model_runner.py`](../../../tests/unit/v1/worker/test_attention_model_runner.py) |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L138-L149", + "text": "| [`v1/worker/attention_model_runner.py`](../../../afd_plugin/v1/worker/attention_model_runner.py) | Disables vLLM 0.26's block-table-only Attention metadata cache only while multiple DBO ubatches are built, then restores every affected builder in `finally`. Remove this workaround when [vLLM #48659](https://github.com/vllm-project/vllm/pull/48659), or an equivalent ubatch-aware cache key, reaches the pinned release. |\n\nSeveral scoped upstream adaptations live with their semantic owners rather\nthan this patch directory. Model dummy runs and V2 execution temporarily wrap\n`create_forward_context`; CUDA worker startup temporarily substitutes the V1\nor V2 runner symbol; V2 graph capture temporarily wraps native input\npreparation; and full-graph replay installs an instance-local manager hook.\nEvery scope restores symbols and runner state in `finally`. Connector process\ngroups also call private PyTorch/vLLM helpers as described in\n[connector contracts](connector_contracts.md). All of these seams must be\nincluded in an upstream upgrade audit even though they are not process-global\nimport-time patches." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L131-L137", + "text": "CUDA scopes a native runner-class substitution around `Worker.init_device()`\nand selects `AFDAttentionModelRunner` or `AFDAttentionModelRunnerV2` from the\nupstream `use_v2_model_runner` setting. NPU applies the AFD-scoped\nvLLM-Ascend patches, fixes the all-to-all backend when required, initializes\nthe vLLM workspace manager, and directly constructs the matching V1 or V2 NPU\nrunner. Their exact inheritance and device mechanisms are specified in\n[execution platforms](execution_platforms.md)." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L322-L327", + "text": "V2 rejects DBO and ubatching. For full-graph capture, the V2 runner wraps the\nnative capture input-preparation seam and publishes exactly one warmup and one\ncapture payload for every native descriptor before the graph body. Native\nfull-graph replay does not create a `ForwardContext`, so an execute-scoped\nmanager hook publishes the padded control shape immediately before replay.\nAll temporary hooks and pending metadata state are restored in `finally`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L132-L139", + "text": "Both CUDA workers delegate device and distributed setup to native\n`Worker.init_device()`. During that synchronous construction window, they\ntemporarily replace the selected upstream runner symbol with the AFD runner\nand restore it in `finally`. V1 Attention selects `AFDAttentionModelRunner`;\nV2 selects `AFDAttentionModelRunnerV2`; a V2-paired FFN still selects the\nminimal `GPUFFNModelRunner`. `torch.accelerator.empty_cache()` releases\nstartup allocations. Attention retains the matching upstream request,\nKV-cache, sampling, and output behavior; FFN remains connector-driven." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L158-L164", + "text": "ModelRunnerV2 rejects ubatching and reuses the native full-graph manager. Its\nAttention wrapper observes each native capture descriptor at the exact input\npreparation seam, publishes a warmup and capture payload outside the graph,\nand suppresses duplicate context sends. Full-graph replay bypasses context\ncreation, so an instance-scoped `run_fullgraph` hook publishes the padded\nshape immediately before replay. The wrapper verifies descriptor order and\ncall count and restores all temporary symbols and state in `finally`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L270-L273", + "text": "The common process-group helper creates a plugin-owned group while temporarily\nswitching PyTorch's default group so vLLM parallel-state helpers initialize\nagainst it. It currently calls private PyTorch/vLLM symbols and is therefore an\nupgrade-sensitive compatibility boundary, not a third-party process-group API." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L320-L321", + "text": "runtimes. A process-group change requires review against the pinned PyTorch and\nvLLM private symbols. Platform and multi-rank claims require the matching E2E" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I15 — patch 目录之外的上游 seam 也在升级审计范围内\n\n- 除了 `afd_plugin/compat/patches/**`,下面这些作用域内的适配也依赖上游内部或私有符号。升级 vLLM 或 PyTorch 时要逐个核对:\n - `afd_plugin/v1/worker/attention_model_runner.py`:在构建多个 DBO ubatch 期间,关闭 vLLM 0.26 只按 block-table 生效的 Attention metadata cache。等 vLLM #48659 或等价修复进入 pinned release 后删除这段规避代码。\n - CUDA worker 在 `Worker.init_device()` 期间临时替换 runner symbol:Attention 换成 `AFDAttentionModelRunner` 或 `AFDAttentionModelRunnerV2`,FFN 换成 `GPUFFNModelRunner`。\n - 几个临时 hook:V2 执行和 model dummy run 对 `create_forward_context` 的包装;V2 graph capture 对原生 input-preparation seam 的包装;full-graph replay 用的实例级 `run_fullgraph` hook。\n - `afd_plugin/distributed/afd_process_group.py` 调用的私有 PyTorch/vLLM helper。\n- 这些 seam 不是 import 期的全局 patch,也不在 patch inventory 里,升级 PR 很容易漏掉。\n- 每个作用域都必须在 `finally` 中恢复符号和 runner 状态。新增 seam 的 PR 也要满足这一点,并补上恢复路径和错误路径的测试。\n\n\n" + }, + "id": "init-bad-skeleton-AFD-I15", + "note": "kb init skeleton: judge said fail; on main the text is changed", + "rule_id": "AFD-I15", + "source": "judge", + "stage": "skeleton", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-skeleton-AFD-I20.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-skeleton-AFD-I20.json new file mode 100644 index 00000000..9354731c --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-skeleton-AFD-I20.json @@ -0,0 +1,57 @@ +{ + "base": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L65-L68", + "text": "Known gaps:\n\n- vLLM versions other than `0.26.0` are not claimed as supported.\n- vLLM/vLLM-Ascend model runner v2 is not supported." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L180-L185", + "text": "GPU model runner v2 is not supported. Select model runner v1 before starting\neither GPU role:\n\n```bash\nexport VLLM_USE_V2_MODEL_RUNNER=0\n```" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L131-L133", + "text": "CUDA scopes a native runner-class substitution around `Worker.init_device()`\nand selects `AFDAttentionModelRunner` or `AFDAttentionModelRunnerV2` from the\nupstream `use_v2_model_runner` setting. NPU applies the AFD-scoped" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L139-L162", + "text": "## ModelRunnerV2 boundary\n\nModelRunnerV2 is an upstream runtime selection, not an AFD configuration key.\nOn Attention, the V2 runners are thin subclasses of the native CUDA or Ascend\nrunner: native V2 retains request state, input preparation, Attention/KV\nexecution, sampling, and output ownership. `AFDMetadataProviderMixin` adds the\nconnector-facing sidecar, DP control payload, transaction IDs, and capture\nmetadata without porting V1 execution methods.\n\nThe supported V2 deployment is deliberately narrower than V1:\n\n| Constraint | CUDA V2 | Ascend V2 |\n| --- | --- | --- |\n| Connector | Synchronous `P2pNcclAFDConnector` | Synchronous `CAMP2pAFDConnector` |\n| Gate placement | `compute_gate_on_attention=false` | `compute_gate_on_attention=false` |\n| Parallelism | PP=PCP=DCP=1; configured role ranks equal DP x TP; static EP enabled | Same |\n| Excluded features | Elastic EP, EPLB, sequence-parallel MoE, compile SP, DBO, and ubatching | Same |\n| Graph execution | Eager or `FULL_DECODE_ONLY` | Eager, `FULL`, or `FULL_DECODE_ONLY` |\n| Model | Must resolve to a registered AFD architecture | Same |\n\nBoth roles validate the paired V2 topology, but only Attention uses the native\nV2 model runner. FFN remains connector-driven and uses its existing AFD runner\nsurface. CUDA V2 has DP2/TP2 eager and graph E2E evidence; the current Ascend\nV2 contract has focused unit evidence but no repository hardware E2E case." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L129-L148", + "text": "## ModelRunnerV2 pairing\n\nFFN remains connector-driven when the upstream configuration selects\nModelRunnerV2. It does not adopt native V2 request state or scheduler-driven\nexecution:\n\n- CUDA temporarily substitutes `GPUFFNModelRunner` at the native V2\n construction seam inside `Worker.init_device()`; the existing AFD runner is\n still the runtime object.\n- Ascend always constructs `AFDNPUFFNModelRunner`. When V2 is selected, its\n connector-driven forward enters the upstream MRV2 forward-context shape so\n Ascend-specific state is installed in `additional_kwargs`.\n- Both workers run the same V2 topology and feature validator as the paired\n Attention role before constructing communication resources.\n\nThe V2 pair therefore requires the synchronous platform connector,\n`compute_gate_on_attention=false`, PP/PCP/DCP size 1, configured role ranks\nequal to DP x TP, static EP, a registered AFD model, and no DBO or ubatching.\nCUDA uses `P2pNcclAFDConnector`; Ascend uses `CAMP2pAFDConnector`. These limits\ndescribe the pair even though only Attention inherits a native V2 runner." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L341-L348", + "text": "| CUDA V2 + `P2pNcclAFDConnector` | Eager or `FULL_DECODE_ONLY` native V2 CUDA Graph | DBO and ubatching rejected | `compute_gate_on_attention=false`; PP/CP, elastic EP, EPLB, SP MoE, and compile SP rejected; role ranks equal DP x TP | DeepSeek-V2-Lite eager/graph DP2 and TP2 accuracy E2E, plus focused V2 unit tests |\n| Ascend V1 + `CAMP2pAFDConnector` | Eager or current ACL Graph path | Native DBO, exactly two ubatches | Common and connector-local `compute_gate_on_attention=false`; `connector_extra_config.quant_mode=0`; plugin CANN ops required | Backend-neutral DeepSeek-V2-Lite eager/graph/DBO accuracy cases plus NPU runtime, graph, ops, connector, and profiler unit tests |\n| Ascend V2 + `CAMP2pAFDConnector` | Eager, `FULL`, or `FULL_DECODE_ONLY` native V2 ACL Graph | DBO and ubatching rejected | `compute_gate_on_attention=false`; PP/CP, elastic EP, EPLB, SP MoE, and compile SP rejected; role ranks equal DP x TP | Focused runner, context, validation, and device-contract unit tests; no repository hardware E2E case |\n| Ascend + `CAMAsyncAFDConnector` | Eager only | Native DBO rejected; optional AFD-managed MoE ubatching uses exactly two request or token-balanced stages | Experimental v0.26 port; `async=true`; documented path uses common `compute_gate_on_attention=true`; token mode requires Attention TP > 1; model runner v1 PCP is unsupported; prefill and decode context parallelism are unsupported; `connector_extra_config.dynamicQuant` is 0 or 1; plugin-owned async CAM ops required | Focused unit coverage; pre-fix DP3TP2/EP2 six-case E2E matrix; post-fix full 61-layer DP2TP8+EP16 token-split run reached `0.9522` strict match on the complete GSM8K evaluation |\n\nAll rows target vLLM 0.26.0. Hardware validation exists for CUDA V1/V2 and the\nrecorded Ascend V1 paths; the Ascend V2 row is an implemented, unit-tested\ncontract rather than a hardware-validated claim. GPU/NPU rank topology and" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/e2e_testing.md:L105-L111", + "text": "| `afd-v2-eager-dp2` | CUDA ModelRunnerV2 eager, Attention DP2 + FFN DP2 | 4 | V2 data-parallel lifecycle and accuracy. |\n| `afd-v2-graph-dp2` | CUDA ModelRunnerV2 graph, Attention DP2 + FFN DP2 | 4 | V2 data-parallel full-decode graph path. |\n| `afd-v2-eager-tp2` | CUDA ModelRunnerV2 eager, Attention TP2 + FFN TP2 | 4 | V2 tensor-parallel lifecycle and accuracy. |\n| `afd-v2-graph-tp2` | CUDA ModelRunnerV2 graph, Attention TP2 + FFN TP2 | 4 | V2 tensor-parallel full-decode graph path. |\n\nCUDA CI runs the legacy and ModelRunnerV2 matrices as separate jobs with a\n40-minute timeout each. Put slower coverage in a scheduled job." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/e2e_testing.md:L147-L151", + "text": "The ModelRunnerV2 matrix is CUDA-only in the current E2E harness. Its local\n1A1F cases are `afd-v2-eager-1a1f` and `afd-v2-graph-1a1f`; CI selects the four\nDP2/TP2 cases listed above by exact node ID on `l4_4`. The Ascend\nModelRunnerV2 implementation currently has focused unit evidence but no E2E\ncase, so it is not included in the hardware gate." + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n\n## AFD-I20 — README 和设计文档对 ModelRunnerV2 是否支持说法矛盾,评审要按窄契约两边一起核对\n\n - README 的 Known gaps 和 Using the Plugin 两节都说 vLLM/vLLM-Ascend model runner v2 不受支持,并要求设置 `VLLM_USE_V2_MODEL_RUNNER=0`。\n - Attention、FFN、平台和 E2E 设计文档则记录:CUDA V2(`P2pNcclAFDConnector`)有 DP2/TP2 的 eager 和 graph E2E,并已进入 CI 门禁;Ascend V2(`CAMP2pAFDConnector`)只有单元测试证据。\n- 涉及 V2 的 PR 不能只凭其中一处下结论。改变 V2 行为或支持声明时,README 和设计文档要同步修改。不得声称 Ascend V2 已经过硬件验证。\n- V2 是上游的 runtime 选择(`use_v2_model_runner`),不是 AFD 配置键。\n- V2 的窄契约由两个角色共同校验,包括:同步 connector、`compute_gate_on_attention=false`、PP=PCP=DCP=1、角色 rank 数等于 DP x TP、静态 EP、不开 DBO/ubatching、CUDA 只允许 eager 或 `FULL_DECODE_ONLY`。\n- 只放宽 Attention 侧的 PR 会破坏 FFN 的配对校验。\n\n\n" + }, + "id": "init-bad-skeleton-AFD-I20", + "note": "kb init skeleton: judge said fail; on main the text is absent", + "rule_id": "AFD-I20", + "source": "judge", + "stage": "skeleton", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-skeleton-AFD-I21.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-skeleton-AFD-I21.json new file mode 100644 index 00000000..2f0351dd --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-skeleton-AFD-I21.json @@ -0,0 +1,52 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L77-L83", + "text": "| Role | CUDA | Ascend |\n| --- | --- | --- |\n| Attention worker | `AFDAttentionWorker(Worker)` | `AFDNPUAttentionWorker(NPUWorker)` |\n| Attention runner V1 | `AFDAttentionModelRunner(GPUModelRunner)` | `AFDNPUAttentionModelRunner(NPUModelRunner)` |\n| Attention runner V2 | `AFDAttentionModelRunnerV2(GPUModelRunnerV2)` | `AFDNPUAttentionModelRunnerV2(NPUModelRunnerV2)` |\n| FFN worker | `AFDFFNWorker(Worker)` | `AFDNPUFFNWorker(NPUWorker)` |\n| FFN runner for V1/V2 pairs | plugin-owned minimal `GPUFFNModelRunner` | `AFDNPUFFNModelRunner(NPUModelRunner)` |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L132-L139", + "text": "Both CUDA workers delegate device and distributed setup to native\n`Worker.init_device()`. During that synchronous construction window, they\ntemporarily replace the selected upstream runner symbol with the AFD runner\nand restore it in `finally`. V1 Attention selects `AFDAttentionModelRunner`;\nV2 selects `AFDAttentionModelRunnerV2`; a V2-paired FFN still selects the\nminimal `GPUFFNModelRunner`. `torch.accelerator.empty_cache()` releases\nstartup allocations. Attention retains the matching upstream request,\nKV-cache, sampling, and output behavior; FFN remains connector-driven." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L369-L371", + "text": "- `PLAT-INV-001`: CUDA and Ascend AFD classes do not inherit from each other;\n platform extensions use matching upstream classes, while\n `GPUFFNModelRunner` remains a plugin-owned minimal runner." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L73-L82", + "text": "| Platform | Worker | Model runner | Current connectors |\n| --- | --- | --- | --- |\n| CUDA | `afd_plugin.v1.worker.AFDFFNWorker` | `GPUFFNModelRunner` | `P2pNcclAFDConnector` |\n| NPU | `afd_plugin.v1.worker.npu.AFDNPUFFNWorker` | `AFDNPUFFNModelRunner` | `CAMP2pAFDConnector`, `CAMAsyncAFDConnector` |\n\nGPU and NPU runtimes use separate internal class paths. `AFDNPUFFNModelRunner`\ninherits vLLM-Ascend `NPUModelRunner` directly instead of inheriting the GPU\n`GPUFFNModelRunner`. Shared AFD semantics are kept in config, connector,\nmetadata, validation, and small helper functions rather than through a\ncross-device inheritance chain." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L115-L118", + "text": "FFN does not own request KV blocks. Both workers return an empty KV-cache spec,\nand both runners no-op KV-cache initialization. `compile_or_warm_up_model()`\nreturns `0.0`; warmup and graph capture, when supported, are driven later by\nconnector metadata. Because the FFN EngineCore skips that upstream warmup path," + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L131-L137", + "text": "FFN remains connector-driven when the upstream configuration selects\nModelRunnerV2. It does not adopt native V2 request state or scheduler-driven\nexecution:\n\n- CUDA temporarily substitutes `GPUFFNModelRunner` at the native V2\n construction seam inside `Worker.init_device()`; the existing AFD runner is\n still the runtime object." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L356-L358", + "text": "## Upstream relationship and validation requirements\n\nChanges must be compared with the pinned vLLM worker and model-runner symbols." + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I21 — CUDA FFN 的 `GPUFFNModelRunner` 不继承上游 runner:上游 Worker 调用 runner 的接口变了,它不会自动跟上\n\n- 其他 AFD runner 都继承对应的上游类:`GPUModelRunner`、`GPUModelRunnerV2`、`NPUModelRunner`、`NPUModelRunnerV2`。\n- CUDA FFN 是例外:无论 V1 还是 V2,都在构造 seam 处换成插件自有的最小 `GPUFFNModelRunner`。PLAT-INV-001 明确保留了这个例外。\n- 因此,上游 `Worker` 对 runner 的调用如果新增或改名(例如 KV-cache spec、`compile_or_warm_up_model()`),`GPUFFNModelRunner` 不会自动继承,只会在运行时报 `AttributeError` 或签名错误。\n- 升级 vLLM 时,要对照 pinned 版本的 `Worker` 核对 FFN runner 暴露的接口,并保持现有语义:\n - 返回空的 KV-cache spec;\n - KV-cache 初始化是 no-op;\n - `compile_or_warm_up_model()` 返回 `0.0`。\n- 反过来,NPU 的 `AFDNPUFFNModelRunner` 直接继承 `NPUModelRunner`,上游新增的行为会自动进入 FFN 路径。要确认这些行为没有带进请求、KV 或调度方面的假设。\n\n\n" + }, + "id": "init-bad-skeleton-AFD-I21", + "note": "kb init skeleton: judge said fail; on main the text is changed", + "rule_id": "AFD-I21", + "source": "judge", + "stage": "skeleton", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-bad-skeleton-AFD-I22.json b/adapters/afd_plugin/kb-calibration/cases/init-bad-skeleton-AFD-I22.json new file mode 100644 index 00000000..6f8acab8 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-bad-skeleton-AFD-I22.json @@ -0,0 +1,57 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/e2e_testing.md:L55-L57", + "text": "This contract is independent of GitHub Actions and Buildkite. CI selects pytest\nnode IDs and supplies hardware paths; it does not redefine a case. Unit tests,\noperator tests, benchmarks, and performance tests are outside this scope." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/e2e_testing.md:L74-L75", + "text": "- `E2E-INV-001` — A case **MUST** have a stable lower-kebab-case ID and cover\n behavior not already covered by an existing case." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/e2e_testing.md:L91-L92", + "text": "- `E2E-INV-007` — CI **MUST NOT** lower the sample count, accuracy threshold,\n or required case set. It **MUST** select cases by pytest node ID." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/e2e_testing.md:L147-L151", + "text": "The ModelRunnerV2 matrix is CUDA-only in the current E2E harness. Its local\n1A1F cases are `afd-v2-eager-1a1f` and `afd-v2-graph-1a1f`; CI selects the four\nDP2/TP2 cases listed above by exact node ID on `l4_4`. The Ascend\nModelRunnerV2 implementation currently has focused unit evidence but no E2E\ncase, so it is not included in the hardware gate." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/e2e_testing.md:L190-L199", + "text": "## Adding a case\n\n1. State the coverage gap and why an existing case cannot catch it.\n2. Choose one stable case ID and one fixed configuration.\n3. Confirm platforms, devices, ports, CI tier, and runtime budget.\n4. Reuse `runner.py`; add a task adapter only for a new evaluation task.\n5. Add focused unit tests for new commands, topology, flags, and failure paths.\n6. Run the case on every selected platform. Record versions, duration, result.\n7. Update `tests/e2e/README.md`. Update the `run-e2e` skill only when commands,\n variables, prerequisites, or the default suite change." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L27-L34", + "text": "validation_paths:\n - \"tests/unit/v1/worker/test_attention_model_runner.py\"\n - \"tests/unit/v1/worker/test_model_runner_v2.py\"\n - \"tests/unit/v1/worker/test_npu_device_contract.py\"\n - \"tests/unit/v1/worker/test_npu_runtime.py\"\n - \"tests/e2e/models/deepseek_v2_lite/test_deepseek_v2_lite.py\"\n - \"tests/e2e/accuracy/**\"\n - \"tests/e2e/models/deepseek_v2_lite/test_async_cam_npu.py\"" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L19-L23", + "text": "validation_paths:\n - \"tests/unit/connectors/**\"\n - \"tests/e2e/models/deepseek_v2_lite/test_deepseek_v2_lite.py\"\n - \"tests/e2e/models/deepseek_v2_lite/test_async_cam_npu.py\"\n - \"tests/e2e/models/deepseek_v2_lite/test_async_cam_npu.py\"" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L54-L63", + "text": "## Implementation evidence\n\n| Area | Source | Focused validation |\n| --- | --- | --- |\n| Base surface and lazy factory | [`base.py`](../../../afd_plugin/connectors/base.py), [`factory.py`](../../../afd_plugin/connectors/factory.py) | [`test_base_factory.py`](../../../tests/unit/connectors/test_base_factory.py) |\n| Payloads and control-plane codec | [`metadata.py`](../../../afd_plugin/connectors/metadata.py) | Connector unit tests and [`test_forward_context.py`](../../../tests/unit/model_executor/models/test_forward_context.py) |\n| CUDA P2P | [`gpu/p2p.py`](../../../afd_plugin/connectors/gpu/p2p.py), [`topology.py`](../../../afd_plugin/distributed/topology.py) | [`test_p2p_connector.py`](../../../tests/unit/connectors/test_p2p_connector.py), [DeepSeek-V2-Lite E2E](../../../tests/e2e/models/deepseek_v2_lite/test_deepseek_v2_lite.py) |\n| Ascend CAMP2P | [`npu/camp2p.py`](../../../afd_plugin/connectors/npu/camp2p.py) | [`test_camp2p_connector.py`](../../../tests/unit/connectors/test_camp2p_connector.py), [DeepSeek-V2-Lite E2E](../../../tests/e2e/models/deepseek_v2_lite/test_deepseek_v2_lite.py) |\n| Ascend CAM async | [`npu/async_cam.py`](../../../afd_plugin/connectors/npu/async_cam.py) | [`test_async_cam_connector.py`](../../../tests/unit/connectors/test_async_cam_connector.py), [`test_async_cam_npu.py`](../../../tests/e2e/models/deepseek_v2_lite/test_async_cam_npu.py) |\n| Process-group construction | [`afd_process_group.py`](../../../afd_plugin/distributed/afd_process_group.py) | Connector initialization tests plus platform E2E paths |" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I22 — E2E case ID 或测试路径改名时,要同步 CI 的 node ID 和各处文档登记\n\n- CI 只按 pytest node ID 选择 E2E case。例如 CUDA V2 的四个 DP2/TP2 case 是在 `l4_4` 上按精确 node ID 选中的。\n- case ID 必须稳定,并采用 lower-kebab-case。重命名测试文件、case ID 或参数化 ID,会让 CI 的选择悄悄失效,代码本身不会报错。\n - CI 配置(`.buildkite/`、`.github/`)里的 node ID;\n - `tests/e2e/README.md`;\n - `.agents/skills/run-e2e/SKILL.md`(仅当命令、变量或默认套件有变化时);\n - 设计文档 front matter 里的 `validation_paths`,以及 Implementation evidence 表中的链接。\n- 不得借改名降低样本数、降低准确率阈值,或删减必需的 case 集。\n\n\n" + }, + "id": "init-bad-skeleton-AFD-I22", + "note": "kb init skeleton: judge said fail; on main the text is changed", + "rule_id": "AFD-I22", + "source": "judge", + "stage": "skeleton", + "verdict": "fail" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I15.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I15.json new file mode 100644 index 00000000..1267a70f --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I15.json @@ -0,0 +1,47 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L42-L48", + "text": "def _is_moe_layer(config: _DeepseekAdapterConfig, layer_idx: int) -> bool:\n moe_layer_freq = getattr(config, \"moe_layer_freq\", 1)\n return (\n config.n_routed_experts is not None\n and layer_idx >= config.first_k_dense_replace\n and layer_idx % moe_layer_freq == 0\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L70-L115", + "text": "def _checkpoint_weight_roles(\n name: str,\n config: _DeepseekAdapterConfig,\n *,\n compute_gate_on_attention: bool,\n attention_shared_experts: bool = False,\n) -> frozenset[str]:\n \"\"\"Classify one native checkpoint path by its AFD execution owner.\"\"\"\n layer_path = _weight_layer_path(name)\n if layer_path is None:\n return _BOTH_ROLES\n\n layer_idx, stage, remainder = layer_path\n if stage == \"self_attn\":\n return _ATTENTION_ROLE\n if stage != \"mlp\":\n return _BOTH_ROLES\n\n if not _is_moe_layer(config, layer_idx):\n return _ATTENTION_ROLE if compute_gate_on_attention else _FFN_ROLE\n\n if attention_shared_experts and remainder and remainder[0] == \"shared_experts\":\n return _ATTENTION_ROLE\n is_moe_gate = bool(remainder) and remainder[0] == \"gate\"\n if is_moe_gate and compute_gate_on_attention:\n return _BOTH_ROLES\n return _FFN_ROLE\n\n\ndef _iter_role_weights(\n weights: Iterable[tuple[str, torch.Tensor]],\n *,\n role: str,\n config: _DeepseekAdapterConfig,\n compute_gate_on_attention: bool,\n attention_shared_experts: bool = False,\n) -> Iterator[tuple[str, torch.Tensor]]:\n \"\"\"Consume a checkpoint iterator once and retain this role's paths.\"\"\"\n for name, loaded_weight in weights:\n if role in _checkpoint_weight_roles(\n name,\n config,\n compute_gate_on_attention=compute_gate_on_attention,\n attention_shared_experts=attention_shared_experts,\n ):\n yield name, loaded_weight" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L213-L229", + "text": " self.shared_experts = (\n native.DeepseekV2MLP(\n hidden_size=config.hidden_size,\n intermediate_size=config.moe_intermediate_size\n * config.n_shared_experts,\n hidden_act=config.hidden_act,\n quant_config=vllm_config.quant_config,\n is_sequence_parallel=True,\n prefix=f\"{prefix}.shared_experts\",\n )\n if (\n config.n_shared_experts\n and parse_afd_config(vllm_config, validate=False).connector\n == AFD_ASYNC_CONNECTOR\n )\n else None\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L367-L371", + "text": " is_moe_layer = (\n config.n_routed_experts is not None\n and layer_idx >= config.first_k_dense_replace\n and layer_idx % moe_layer_freq == 0\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L439-L460", + "text": " elif self.compute_gate_on_attention and self.is_moe_layer:\n self.mlp = GateOnlyRemoteMoE(\n config=config,\n layer_idx=layer_idx,\n prefix=f\"{prefix}.mlp\",\n vllm_config=vllm_config,\n )\n elif self.compute_gate_on_attention:\n self.mlp = native.DeepseekV2MLP(\n hidden_size=config.hidden_size,\n intermediate_size=config.intermediate_size,\n hidden_act=config.hidden_act,\n quant_config=quant_config,\n prefix=f\"{prefix}.mlp\",\n )\n else:\n self.mlp = RemoteFFNProxy(layer_idx=layer_idx)\n\n elif afd_role == \"ffn\":\n self.self_attn = native.PPMissingLayer()\n if self.compute_gate_on_attention and not self.is_moe_layer:\n self.mlp = native.PPMissingLayer()" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L858-L868", + "text": " def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:\n return super().load_weights(\n _iter_role_weights(\n weights,\n role=self.afd_role,\n config=self.config,\n compute_gate_on_attention=self.afd_config.compute_gate_on_attention,\n attention_shared_experts=self.afd_config.connector\n == AFD_ASYNC_CONNECTOR,\n )\n )" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n" + }, + "id": "init-good-deepen-AFD-I15", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I15", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I21.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I21.json new file mode 100644 index 00000000..6578170a --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I21.json @@ -0,0 +1,42 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L128-L159", + "text": " def _send_and_receive(\n self,\n hidden_states: torch.Tensor,\n **send_kwargs: torch.Tensor,\n ) -> torch.Tensor:\n afd_metadata = get_afd_metadata_from_forward_context()\n if afd_metadata is None:\n raise RuntimeError(\"RemoteFFNProxy requires AFD forward metadata\")\n forward_context = get_forward_context()\n stage_idx = int(\n getattr(forward_context, \"ubatch_idx\", afd_metadata.stage_idx),\n )\n afd_metadata.stage_idx = stage_idx\n metadata = AFDTransferMetadata.create_attention_metadata(\n layer_idx=self.layer_idx,\n stage_idx=stage_idx,\n seq_len=int(hidden_states.shape[0]),\n )\n context = AFDTransferContext(metadata=metadata)\n afd_metadata.connector.send_attn_output(\n hidden_states,\n context,\n **send_kwargs,\n )\n hidden_states = maybe_apply_dbo_yield(\n hidden_states,\n role=\"attention\",\n )\n return afd_metadata.connector.recv_ffn_output(\n ref_tensor=hidden_states,\n ubatch_idx=stage_idx,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L176-L189", + "text": " def forward( # type: ignore[override]\n self,\n hidden_states: torch.Tensor,\n router_logits: torch.Tensor,\n input_ids: torch.Tensor | None = None,\n ) -> torch.Tensor:\n if input_ids is not None:\n raise NotImplementedError(\n \"experts-boundary input_ids transport is not implemented\",\n )\n send_kwargs = (\n {} if self.is_internal_router else {\"router_logits\": router_logits}\n )\n return self._send_and_receive(hidden_states, **send_kwargs)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v4.py:L82-L120", + "text": " def forward(\n self,\n hidden_states: torch.Tensor,\n input_ids: torch.Tensor | None,\n ) -> torch.Tensor:\n if input_ids is None:\n raise RuntimeError(\"DeepSeek-V4 remote FFN requires input_ids\")\n if input_ids.ndim != 1 or input_ids.shape[0] != hidden_states.shape[0]:\n raise ValueError(\n \"DeepSeek-V4 input_ids must be one-dimensional and token-aligned\",\n )\n\n afd_metadata = get_afd_metadata_from_forward_context()\n if afd_metadata is None:\n raise RuntimeError(\"RemoteDeepseekV4FFN requires AFD forward metadata\")\n forward_context = get_forward_context()\n stage_idx = int(\n getattr(forward_context, \"ubatch_idx\", afd_metadata.stage_idx),\n )\n afd_metadata.stage_idx = stage_idx\n metadata = AFDTransferMetadata.create_attention_metadata(\n layer_idx=self.layer_idx,\n stage_idx=stage_idx,\n seq_len=int(hidden_states.shape[0]),\n )\n context = AFDTransferContext(metadata=metadata)\n afd_metadata.connector.send_attn_output(\n hidden_states,\n context,\n input_ids=input_ids,\n )\n hidden_states = maybe_apply_dbo_yield(\n hidden_states,\n role=\"attention\",\n )\n return afd_metadata.connector.recv_ffn_output(\n ref_tensor=hidden_states,\n ubatch_idx=stage_idx,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v4.py:L302-L313", + "text": " def compute_ffn_output(\n self,\n hidden_states: torch.Tensor,\n *,\n input_ids: torch.Tensor | None,\n ) -> torch.Tensor:\n \"\"\"Execute the complete native V4 MoE, including its native router.\"\"\"\n if not isinstance(self.ffn, native.DeepseekV4MoE):\n raise RuntimeError(\"DeepSeek-V4 FFN compute is FFN-role only\")\n if input_ids is None:\n raise RuntimeError(\"DeepSeek-V4 FFN compute requires input_ids\")\n return self.ffn(hidden_states, input_ids)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v4.py:L476-L499", + "text": "class AFDDeepseekV4ForCausalLM(native.DeepseekV4ForCausalLM):\n \"\"\"DeepSeek-V4 causal LM exposing the GPU FFN-runner model contract.\"\"\"\n\n model_cls = AFDDeepseekV4Model\n afd_requires_input_ids = True\n\n def __init__(self, *, vllm_config: VllmConfig, prefix: str = \"\") -> None:\n self.afd_config = parse_afd_config(vllm_config, validate=False)\n self.afd_role = self.afd_config.role\n super().__init__(vllm_config=vllm_config, prefix=prefix)\n\n def compute_ffn_output(\n self,\n hidden_states: torch.Tensor,\n layer_idx: int,\n *,\n input_ids: torch.Tensor | None = None,\n **kwargs: Any,\n ) -> torch.Tensor:\n return self.model.compute_ffn_output(\n hidden_states,\n layer_idx,\n input_ids=input_ids,\n )" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n" + }, + "id": "init-good-deepen-AFD-I21", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I21", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I22.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I22.json new file mode 100644 index 00000000..6eafea6f --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I22.json @@ -0,0 +1,47 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L186-L192", + "text": " send_kwargs = (\n {} if self.is_internal_router else {\"router_logits\": router_logits}\n )\n return self._send_and_receive(hidden_states, **send_kwargs)\n\n def update_expert_map(self) -> None:\n \"\"\"Satisfy the native EPLB model interface without local experts.\"\"\"" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L289-L316", + "text": " router_dtype = native._get_moe_router_dtype(config)\n if compute_gate_on_attention:\n self.gate = native.GateLinear(\n config.hidden_size,\n config.n_routed_experts,\n out_dtype=router_dtype,\n prefix=f\"{prefix}.gate\",\n )\n if getattr(config, \"topk_method\", None) == \"noaux_tc\":\n self.gate.e_score_correction_bias = nn.Parameter(\n torch.empty(config.n_routed_experts, dtype=torch.float32),\n )\n else:\n self.gate.e_score_correction_bias = None\n else:\n self.gate = None\n\n ep_size = native.get_ep_group().device_group.size()\n self.n_routed_experts = int(config.n_routed_experts)\n self.n_shared_experts = int(config.n_shared_experts)\n self.n_redundant_experts = parallel_config.eplb_config.num_redundant_experts\n self.n_logical_experts = self.n_routed_experts\n self.n_physical_experts = self.n_logical_experts + self.n_redundant_experts\n self.n_local_physical_experts = self.n_physical_experts // ep_size\n self.experts = AFDAttentionFusedMoE(\n layer_idx=layer_idx,\n is_internal_router=not compute_gate_on_attention,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L387-L396", + "text": " device_type = native.current_platform.device_type\n self.uses_remote_experts = device_type == \"cuda\" and self.is_moe_layer\n if (\n afd_role == \"attention\"\n and self.uses_remote_experts\n and parallel_config.enable_eplb\n ):\n raise RuntimeError(\n \"CUDA remote experts do not support EPLB on the Attention role\",\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L486-L490", + "text": " if self.compute_gate_on_attention and device_type == \"cuda\":\n # Keep the native gate parameter and path for loader/model\n # compatibility, but configure the runner to consume the\n # router logits transferred from Attention.\n self.mlp.experts.gate = None" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L618-L635", + "text": " def compute_experts_output(\n self,\n hidden_states: torch.Tensor,\n router_logits: torch.Tensor,\n ) -> torch.Tensor:\n \"\"\"Execute the native external-router runner on the FFN role.\"\"\"\n if not self.compute_gate_on_attention or not self.is_moe_layer:\n raise RuntimeError(\n \"compute_experts_output requires an Attention-side-gate MoE layer\",\n )\n if not isinstance(self.mlp, native.DeepseekV2MoE):\n raise RuntimeError(\"FFN role does not own a native DeepSeek MoE\")\n if self.mlp.experts.is_internal_router:\n raise RuntimeError(\"FFN native runner must use external routing\")\n return self.mlp.experts(\n hidden_states=hidden_states,\n router_logits=router_logits,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L783-L801", + "text": " def get_experts_layer_indices(self) -> tuple[int, ...]:\n return tuple(\n layer_idx\n for layer_idx, layer in enumerate(self.layers)\n if isinstance(layer, AFDDeepseekV2DecoderLayer)\n and layer.uses_remote_experts\n )\n\n def get_experts_routing_spec(\n self,\n layer_idx: int,\n ) -> AFDExpertRoutingSpec:\n \"\"\"Return the static native-router contract for graph capture.\"\"\"\n layer = self.layers[layer_idx]\n gate = layer.mlp.gate\n return AFDExpertRoutingSpec(\n router_logits_width=int(layer.mlp.n_routed_experts),\n router_logits_dtype=gate.out_dtype or gate.weight.dtype,\n )" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n" + }, + "id": "init-good-deepen-AFD-I22", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I22", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I24.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I24.json new file mode 100644 index 00000000..bef86de8 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I24.json @@ -0,0 +1,32 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L461-L490", + "text": " elif self.is_moe_layer:\n # vLLM models bind FusedMoE at module import time. AFD can import\n # native DeepSeek before vLLM-Ascend patches the package factory,\n # so refresh that binding after platform initialization and before\n # constructing the NPU FFN MoE.\n if device_type == \"npu\":\n native.FusedMoE = fused_moe.FusedMoE\n moe_config = config\n if afd_config.connector == AFD_ASYNC_CONNECTOR:\n moe_config = copy(config)\n # HF validates this public field as int, but pinned native\n # MoE uses only None to omit shared construction (0 builds\n # a zero-width MLP). Apply its sentinel to this private\n # construction copy; preserve the real model config.\n object.__setattr__(moe_config, \"n_shared_experts\", None)\n self.mlp = native.DeepseekV2MoE(\n config=moe_config,\n parallel_config=parallel_config,\n quant_config=quant_config,\n prefix=f\"{prefix}.mlp\",\n # aiter applies routed_scaling_factor internally\n apply_routed_scale_to_output=(\n not native.rocm_aiter_ops.is_fused_moe_enabled()\n ),\n )\n if self.compute_gate_on_attention and device_type == \"cuda\":\n # Keep the native gate parameter and path for loader/model\n # compatibility, but configure the runner to consume the\n # router logits transferred from Attention.\n self.mlp.experts.gate = None" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L223-L227", + "text": " if (\n config.n_shared_experts\n and parse_afd_config(vllm_config, validate=False).connector\n == AFD_ASYNC_CONNECTOR\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L91-L92", + "text": " if attention_shared_experts and remainder and remainder[0] == \"shared_experts\":\n return _ATTENTION_ROLE" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n" + }, + "id": "init-good-deepen-AFD-I24", + "note": "kb init deepen: judge said pass; on main the text is kept", + "rule_id": "AFD-I24", + "source": "owner", + "stage": "deepen", + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I25.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I25.json new file mode 100644 index 00000000..6ae7d85b --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I25.json @@ -0,0 +1,47 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L269-L317", + "text": " # Patch reason: native DeepseekV2MoE constructs local routed/shared experts.\n # Patch functionality: preserve the native MoE forward contract while\n # constructing only the gate owned by Attention and a parameter-free proxy.\n # Signature: AFD-owned; adds layer_idx and compute_gate_on_attention and omits\n # quant_config because no local expert kernel is constructed.\n # Upstream: vLLM v0.26.0, vllm/model_executor/models/deepseek_v2.py\n # Commit: 568afb3a13806beb53bb2e6bd518269357b237c0\n def __init__(\n self,\n *,\n config: _DeepseekAdapterConfig,\n parallel_config: ParallelConfig,\n layer_idx: int,\n prefix: str,\n compute_gate_on_attention: bool,\n ) -> None:\n # ### PATCH START: construct a remote-experts native MoE shell.\n nn.Module.__init__(self)\n self.is_sequence_parallel = parallel_config.use_sequence_parallel_moe\n\n router_dtype = native._get_moe_router_dtype(config)\n if compute_gate_on_attention:\n self.gate = native.GateLinear(\n config.hidden_size,\n config.n_routed_experts,\n out_dtype=router_dtype,\n prefix=f\"{prefix}.gate\",\n )\n if getattr(config, \"topk_method\", None) == \"noaux_tc\":\n self.gate.e_score_correction_bias = nn.Parameter(\n torch.empty(config.n_routed_experts, dtype=torch.float32),\n )\n else:\n self.gate.e_score_correction_bias = None\n else:\n self.gate = None\n\n ep_size = native.get_ep_group().device_group.size()\n self.n_routed_experts = int(config.n_routed_experts)\n self.n_shared_experts = int(config.n_shared_experts)\n self.n_redundant_experts = parallel_config.eplb_config.num_redundant_experts\n self.n_logical_experts = self.n_routed_experts\n self.n_physical_experts = self.n_logical_experts + self.n_redundant_experts\n self.n_local_physical_experts = self.n_physical_experts // ep_size\n self.experts = AFDAttentionFusedMoE(\n layer_idx=layer_idx,\n is_internal_router=not compute_gate_on_attention,\n )\n # ### PATCH END: construct a remote-experts native MoE shell." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L323-L340", + "text": " # Patch reason: native DeepSeek constructs both Attention and FFN modules.\n # Patch functionality: construct only the modules owned by the active AFD role.\n # Signature: matches upstream; no added parameters.\n # Upstream: vLLM v0.26.0, vllm/model_executor/models/deepseek_v2.py\n # Commit: 568afb3a13806beb53bb2e6bd518269357b237c0\n def __init__(\n self,\n vllm_config: VllmConfig,\n prefix: str,\n config: DeepseekV2Config | None = None,\n topk_indices_buffer: torch.Tensor | None = None,\n ) -> None:\n # ### PATCH START: require an explicit AFD role before allocation.\n afd_config = parse_afd_config(vllm_config, validate=False)\n afd_role = afd_config.role\n\n torch.nn.Module.__init__(self)\n # ### PATCH END" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L644-L666", + "text": " # Patch reason: native DeepSeek always creates native Decoder layers.\n # Patch functionality: create role-aware AFD layers without full allocation.\n # Signature: matches upstream; no added parameters.\n # Upstream: vLLM v0.26.0, vllm/model_executor/models/deepseek_v2.py\n # Commit: 568afb3a13806beb53bb2e6bd518269357b237c0\n def __init__(self, *, vllm_config: VllmConfig, prefix: str = \"\"):\n # ### PATCH START: require AFD activation and avoid native allocation.\n afd_config = parse_afd_config(vllm_config, validate=False)\n if bool(\n getattr(\n vllm_config.parallel_config,\n \"use_sequence_parallel_moe\",\n False,\n ),\n ):\n raise RuntimeError(\n \"AFD DeepSeek does not support sequence-parallel MoE\",\n )\n torch.nn.Module.__init__(self)\n self.vllm_config = vllm_config\n self.compilation_config = vllm_config.compilation_config\n self.afd_config = afd_config\n # ### PATCH END" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L714-L730", + "text": " self.make_empty_intermediate_tensors = (\n native.make_empty_intermediate_tensors_factory(\n [\"hidden_states\", \"residual\"],\n self.hidden_size,\n )\n )\n self.aux_hidden_state_layers = tuple[int, ...]()\n\n # Needed by load_weights\n qk_nope_head_dim = getattr(config, \"qk_nope_head_dim\", 0)\n qk_rope_head_dim = getattr(config, \"qk_rope_head_dim\", 0)\n self.use_mha = config.model_type == \"deepseek\" or all(\n dim == 0 for dim in (qk_nope_head_dim, qk_rope_head_dim)\n )\n self.num_redundant_experts = (\n vllm_config.parallel_config.eplb_config.num_redundant_experts\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v4.py:L126-L142", + "text": " # Patch reason: native DeepSeek-V4 always constructs Attention and FFN.\n # Patch functionality: allocate only the stage owned by the active AFD role.\n # Signature: matches upstream; no added parameters.\n # Upstream: vLLM v0.26.0, vllm/models/deepseek_v4/nvidia/model.py\n # Commit: 568afb3a13806beb53bb2e6bd518269357b237c0\n def __init__(\n self,\n vllm_config,\n prefix,\n topk_indices_buffer: torch.Tensor | None = None,\n aux_stream_list: list[torch.cuda.Stream] | None = None,\n ):\n # ### PATCH START: construct a role-local decoder stage.\n nn.Module.__init__(self)\n afd_config = parse_afd_config(vllm_config, validate=False)\n layer_idx = int(prefix.rsplit(\".\", maxsplit=1)[-1])\n # ### PATCH END" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v4.py:L319-L358", + "text": " # Patch reason: native DeepSeek-V4 allocates every decoder stage and stream.\n # Patch functionality: build role-aware layers and Attention-only resources.\n # Signature: matches upstream; no added parameters.\n # Upstream: vLLM v0.26.0, vllm/models/deepseek_v4/nvidia/model.py\n # Commit: 568afb3a13806beb53bb2e6bd518269357b237c0\n def __init__(self, *, vllm_config: VllmConfig, prefix: str = \"\"):\n # ### PATCH START: validate the deliberately narrow first release.\n nn.Module.__init__(self)\n self.afd_config = parse_afd_config(vllm_config, validate=False)\n if native.current_platform.device_type != \"cuda\":\n raise RuntimeError(\"AFD DeepSeek-V4 supports CUDA only\")\n if self.afd_config.connector != \"P2pNcclAFDConnector\":\n raise RuntimeError(\n \"AFD DeepSeek-V4 requires the synchronous P2pNcclAFDConnector\",\n )\n if self.afd_config.compute_gate_on_attention:\n raise RuntimeError(\n \"AFD DeepSeek-V4 does not support compute_gate_on_attention\",\n )\n parallel_config = vllm_config.parallel_config\n if parallel_config.pipeline_parallel_size != 1:\n raise RuntimeError(\"AFD DeepSeek-V4 does not support PP\")\n if parallel_config.use_sequence_parallel_moe:\n raise RuntimeError(\"AFD DeepSeek-V4 does not support SP MoE\")\n if parallel_config.enable_eplb:\n raise RuntimeError(\"AFD DeepSeek-V4 does not support EPLB\")\n # ### PATCH END\n\n config = vllm_config.model_config.hf_config\n quant_config = vllm_config.quant_config\n self.config = config\n self.quant_config = quant_config\n self.parallel_config = parallel_config\n self.use_mega_moe = (\n vllm_config.kernel_config.moe_backend == \"deep_gemm_mega_moe\"\n )\n # ### PATCH START: MegaMoE has unproven role-local finalization semantics.\n if self.use_mega_moe:\n raise RuntimeError(\"AFD DeepSeek-V4 does not support MegaMoE\")\n # ### PATCH END" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n" + }, + "id": "init-good-deepen-AFD-I25", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I25", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I26.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I26.json new file mode 100644 index 00000000..2b6735c3 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I26.json @@ -0,0 +1,47 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L244-L247", + "text": " def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:\n from afd_plugin.model_executor.models.npu import (\n deepseek_v2_attention_gate,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L551-L556", + "text": " # NPU-only: Attention-side gate/topk is implemented in the NPU helper.\n if self.compute_gate_on_attention and self.is_moe_layer:\n from afd_plugin.model_executor.models.npu import (\n deepseek_v2_attention_gate,\n )\n" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L590-L593", + "text": " # NPU-only: gated MoE FFN compute consumes Attention-side topk payloads.\n from afd_plugin.model_executor.models.npu import (\n deepseek_v2_attention_gate,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L742-L753", + "text": " if self.afd_config.connector == AFD_ASYNC_CONNECTOR:\n from afd_plugin.model_executor.models.npu import (\n deepseek_v2_async_cam_forward,\n )\n\n return deepseek_v2_async_cam_forward.run_model_forward(\n self,\n input_ids,\n positions,\n intermediate_tensors,\n inputs_embeds,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/npu/deepseek_attention_metadata.py:L11-L21", + "text": "from vllm_ascend.attention.dsa_v1 import (\n AscendDSAMetadata,\n AscendDSAMetadataBuilder,\n)\nfrom vllm_ascend.attention.mla_v1 import AscendMLAMetadata\nfrom vllm_ascend.attention.sfa_v1 import (\n AscendSFAMetadata,\n AscendSFAMetadataBuilder,\n)\nfrom vllm_ascend.attention.utils import AscendCommonAttentionMetadata\nfrom vllm_ascend.ops.rope_dsv4 import get_cos_and_sin_dsa" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/npu/deepseek_v2_async_cam_forward.py:L40-L44", + "text": "if TYPE_CHECKING:\n from afd_plugin.model_executor.models.deepseek_v2 import (\n AFDDeepseekV2DecoderLayer,\n AFDDeepseekV2Model,\n )" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n" + }, + "id": "init-good-deepen-AFD-I26", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I26", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I28.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I28.json new file mode 100644 index 00000000..31ccc70a --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I28.json @@ -0,0 +1,27 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/npu/async_cam_layout.py:L132-L235", + "text": "def prepare_cam_dispatch_payload(\n hidden_states: torch.Tensor,\n topk_weights: torch.Tensor,\n topk_ids: torch.Tensor,\n router_logits: torch.Tensor | None,\n *,\n use_sequence_parallel: bool,\n) -> CAMDispatchPayload:\n \"\"\"Convert the model token layout to one rank-local CAM payload.\n\n FlashComm1 already leaves each Attention TP rank with a disjoint token\n shard, so that layout passes through unchanged. Plain TP keeps a replicated\n token dimension after its tensor-parallel collectives; only the CAM\n boundary shards that dimension so each global token is dispatched once.\n \"\"\"\n\n parent_tokens = int(hidden_states.shape[0])\n _require_matching_first_dim(\n topk_weights,\n parent_tokens,\n tensor_name=\"topk_weights\",\n )\n _require_matching_first_dim(\n topk_ids,\n parent_tokens,\n tensor_name=\"topk_ids\",\n )\n if router_logits is not None:\n _require_matching_first_dim(\n router_logits,\n parent_tokens,\n tensor_name=\"router_logits\",\n )\n\n tp_group = get_tp_group()\n tp_rank = int(tp_group.rank_in_group)\n tp_size = int(tp_group.world_size)\n if use_sequence_parallel or tp_size <= 1:\n layout = CAMDispatchLayout(\n parent_tokens=parent_tokens,\n padded_tokens=parent_tokens,\n local_token_slice=slice(0, parent_tokens),\n requires_tp_all_gather=False,\n use_sequence_parallel=use_sequence_parallel,\n tp_rank=tp_rank,\n tp_size=tp_size,\n )\n _log_cam_layout(\"dispatch\", layout)\n return CAMDispatchPayload(\n hidden_states=hidden_states,\n topk_weights=topk_weights,\n topk_ids=topk_ids,\n router_logits=router_logits,\n layout=layout,\n )\n\n local_tokens = (parent_tokens + tp_size - 1) // tp_size\n padded_tokens = local_tokens * tp_size\n local_start = tp_rank * local_tokens\n local_token_slice = slice(local_start, local_start + local_tokens)\n layout = CAMDispatchLayout(\n parent_tokens=parent_tokens,\n padded_tokens=padded_tokens,\n local_token_slice=local_token_slice,\n requires_tp_all_gather=True,\n use_sequence_parallel=False,\n tp_rank=tp_rank,\n tp_size=tp_size,\n )\n _log_cam_layout(\"dispatch\", layout)\n return CAMDispatchPayload(\n hidden_states=_pad_first_dim(hidden_states, padded_tokens)[local_token_slice],\n topk_weights=_pad_first_dim(topk_weights, padded_tokens)[local_token_slice],\n topk_ids=_pad_first_dim(topk_ids, padded_tokens)[local_token_slice],\n router_logits=(\n None\n if router_logits is None\n else _pad_first_dim(router_logits, padded_tokens)[local_token_slice]\n ),\n layout=layout,\n )\n\n\ndef restore_cam_dispatch_output(\n local_output: torch.Tensor,\n layout: CAMDispatchLayout,\n) -> torch.Tensor:\n \"\"\"Restore one CAM result to the token layout expected by the model.\"\"\"\n\n if int(local_output.shape[0]) != layout.local_tokens:\n raise RuntimeError(\n \"CAM output does not match the dispatched rank-local token count: \"\n f\"expected={layout.local_tokens}, actual={int(local_output.shape[0])}\",\n )\n if not layout.requires_tp_all_gather:\n _log_cam_layout(\"restore\", layout)\n return local_output\n\n global_output = _all_gather_rows(\n local_output,\n expected_rows=layout.padded_tokens,\n )\n _log_cam_layout(\"restore\", layout)\n return global_output[: layout.parent_tokens]" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/npu/async_cam_layout.py:L522-L533", + "text": "def _all_gather_rows(\n tensor: torch.Tensor,\n *,\n expected_rows: int,\n) -> torch.Tensor:\n gathered = tensor_model_parallel_all_gather(tensor.contiguous(), 0)\n if int(gathered.shape[0]) != expected_rows:\n raise RuntimeError(\n \"Async CAM TP all-gather returned an unexpected row count: \"\n f\"expected={expected_rows}, actual={int(gathered.shape[0])}\",\n )\n return gathered" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n" + }, + "id": "init-good-deepen-AFD-I28", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I28", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I29.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I29.json new file mode 100644 index 00000000..9cc47bea --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I29.json @@ -0,0 +1,47 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/npu/async_cam_layout.py:L43-L76", + "text": " def __post_init__(self) -> None:\n num_stages = len(self.stages)\n if not num_stages or len(self.attn_metadata) != num_stages:\n raise ValueError(\n \"Async CAM execution-plan fields must describe the same \"\n f\"non-empty stage count: attention={len(self.attn_metadata)}, \"\n f\"slices={num_stages}\",\n )\n\n expected_token_start = 0\n for stage in self.stages:\n token_slice = stage.token_slice\n token_start = int(token_slice.start)\n token_stop = int(token_slice.stop)\n actual_tokens = token_stop - token_start\n input_tokens = int(stage.input_tokens)\n if token_start != expected_token_start or actual_tokens <= 0:\n raise ValueError(\n \"Async CAM stage token slices must be contiguous, ordered, \"\n f\"and non-empty: token_slice={token_slice}, \"\n f\"expected_start={expected_token_start}\",\n )\n if not 0 < actual_tokens <= input_tokens:\n raise ValueError(\n \"Async CAM stage actual-token count must fit its physical \"\n f\"extent: actual={actual_tokens}, input={input_tokens}\",\n )\n expected_token_start = token_stop\n if self.parent_input_tokens < expected_token_start:\n raise ValueError(\n \"Async CAM parent input extent must cover every real token: \"\n f\"parent_input_tokens={self.parent_input_tokens}, \"\n f\"actual_tokens={expected_token_start}\",\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/npu/async_cam_layout.py:L247-L264", + "text": " if not metadata.use_sequence_parallel:\n return _build_replicated_stage_inputs(\n hidden_states,\n residual,\n positions,\n llama_4_scaling,\n metadata,\n )\n\n tp_group = get_tp_group()\n tp_rank = int(tp_group.rank_in_group)\n tp_size = int(tp_group.world_size)\n global_input_tokens = metadata.parent_input_tokens\n if tp_size <= 1 or global_input_tokens % tp_size != 0:\n raise ValueError(\n \"Invalid sequence-parallel Async CAM layout: \"\n f\"global_input_tokens={global_input_tokens}, tp_size={tp_size}\",\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/npu/async_cam_layout.py:L315-L321", + "text": " for stage_slice in metadata.stages:\n stage_input_tokens = int(stage_slice.input_tokens)\n if stage_input_tokens % tp_size != 0:\n raise ValueError(\n \"Async CAM stage extent must be TP divisible: \"\n f\"token_slice={stage_slice.token_slice}, tp_size={tp_size}\",\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/npu/async_cam_layout.py:L386-L441", + "text": "def restore_async_moe_stage_outputs(\n stage_outputs: list[torch.Tensor],\n metadata: AsyncMoeUbatchMetadata,\n) -> torch.Tensor:\n \"\"\"Restore stage-local outputs to the parent model's TP-local layout.\"\"\"\n\n if len(stage_outputs) != len(metadata.stages):\n raise ValueError(\n \"Async CAM stage output count does not match its execution plan: \"\n f\"outputs={len(stage_outputs)}, \"\n f\"stages={len(metadata.stages)}\",\n )\n if not metadata.use_sequence_parallel:\n return _pad_first_dim(\n torch.cat(\n _trim_real_stage_outputs(\n stage_outputs,\n metadata,\n ),\n dim=0,\n ),\n metadata.parent_input_tokens,\n )\n\n tp_group = get_tp_group()\n tp_rank = int(tp_group.rank_in_group)\n tp_size = int(tp_group.world_size)\n global_stage_outputs = [\n _all_gather_rows(\n stage_output,\n expected_rows=int(stage_slice.input_tokens),\n )\n for stage_output, stage_slice in zip(\n stage_outputs,\n metadata.stages,\n strict=True,\n )\n ]\n global_output = _pad_first_dim(\n torch.cat(\n _trim_real_stage_outputs(\n global_stage_outputs,\n metadata,\n ),\n dim=0,\n ),\n metadata.parent_input_tokens,\n )\n if int(global_output.shape[0]) % tp_size != 0:\n raise ValueError(\n \"Restored Async CAM output is not TP divisible: \"\n f\"rows={int(global_output.shape[0])}, tp_size={tp_size}\",\n )\n local_full_tokens = int(global_output.shape[0]) // tp_size\n local_start = tp_rank * local_full_tokens\n return global_output[local_start : local_start + local_full_tokens]" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/npu/async_cam_layout.py:L599-L617", + "text": "def _trim_real_stage_outputs(\n stage_outputs: list[torch.Tensor],\n metadata: AsyncMoeUbatchMetadata,\n) -> list[torch.Tensor]:\n real_stage_outputs = []\n for stage_output, stage in zip(\n stage_outputs,\n metadata.stages,\n strict=True,\n ):\n actual_rows = int(stage_output.shape[0])\n expected_rows = int(stage.input_tokens)\n if actual_rows != expected_rows:\n raise RuntimeError(\n \"Async CAM stage output does not match its physical plan: \"\n f\"expected_rows={expected_rows}, actual_rows={actual_rows}\",\n )\n real_stage_outputs.append(stage_output[: stage.actual_tokens])\n return real_stage_outputs" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/npu/async_cam_layout.py:L635-L655", + "text": "def _optional_global_token_dim(\n tensor: torch.Tensor,\n num_tokens: int,\n *,\n preferred_dim: int | None = None,\n) -> int | None:\n if (\n preferred_dim is not None\n and tensor.dim() > preferred_dim\n and int(tensor.shape[preferred_dim]) == num_tokens\n ):\n return preferred_dim\n # vLLM uses [N] for ordinary positions and [axes, N] for multi-axis\n # positions. Prefer axis 1 for a higher-dimensional tensor so an\n # accidental square shape does not reinterpret the position axes as\n # tokens.\n if tensor.dim() > 1 and int(tensor.shape[1]) == num_tokens:\n return 1\n if tensor.dim() > 0 and int(tensor.shape[0]) == num_tokens:\n return 0\n return None" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n" + }, + "id": "init-good-deepen-AFD-I29", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I29", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I30.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I30.json new file mode 100644 index 00000000..4ff43060 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I30.json @@ -0,0 +1,32 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/npu/deepseek_attention_metadata.py:L24-L49", + "text": "def isolate_deepseek_attention_builder_inputs(\n builder: AttentionMetadataBuilder,\n metadata: AscendCommonAttentionMetadata,\n) -> None:\n \"\"\"Detach common metadata that a DeepSeek builder mutates in place.\n\n Async CAM builds the full batch before both execution stages. Stage\n builders therefore must not write through views into the full-batch\n common metadata. The copies below are deliberately backend-specific;\n the remaining common metadata fields are read-only builder inputs and\n retain their zero-copy views.\n \"\"\"\n\n if isinstance(builder, AscendSFAMetadataBuilder):\n # SFA C8 reshape writes all three arrays through\n # store_kv_block_metadata().\n if metadata.group_len is not None:\n metadata.group_len = metadata.group_len.clone()\n if metadata.group_key_idx is not None:\n metadata.group_key_idx = metadata.group_key_idx.clone()\n if metadata.group_key_cache_idx is not None:\n metadata.group_key_cache_idx = metadata.group_key_cache_idx.clone()\n elif isinstance(builder, AscendDSAMetadataBuilder):\n # DSA clears padded request rows while building prefill/decode\n # metadata. Keep those writes local to this stage and cache group.\n metadata.block_table_tensor = metadata.block_table_tensor.clone()" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/npu/deepseek_attention_metadata.py:L52-L105", + "text": "def materialize_deepseek_attention_metadata(\n metadata: AttentionMetadata,\n input_positions: torch.Tensor,\n) -> None:\n \"\"\"Detach mutable backend workspaces from one metadata object.\n\n vLLM-Ascend metadata builders may return views into process-global RoPE\n runtime buffers. That is safe when one metadata object is live, but Async\n CAM builds full-batch, stage-0, and stage-1 metadata before executing any\n of them. A later build must not change an earlier metadata object.\n\n Only mutable runtime storage is materialized here. Immutable RoPE tables,\n KV-cache tensors, masks, block tables, and other read-only inputs continue\n to be shared with the upstream runtime.\n \"\"\"\n\n if isinstance(metadata, AscendSFAMetadata):\n # SFA always asks get_cos_and_sin_mla() for its reusable runtime\n # buffer, for both prefill and decode.\n metadata.cos = metadata.cos.clone()\n metadata.sin = metadata.sin.clone()\n return\n\n if isinstance(metadata, AscendMLAMetadata):\n # RoPE MLA prefill currently returns fresh indexed tensors, while\n # no-RoPE MLA prefill and every decode use reusable identity/runtime\n # buffers. Materialize both paths so ownership does not depend on\n # that backend implementation detail.\n if metadata.prefill is not None:\n metadata.prefill.cos = metadata.prefill.cos.clone()\n metadata.prefill.sin = metadata.prefill.sin.clone()\n if metadata.decode is not None:\n metadata.decode.cos = metadata.decode.cos.clone()\n metadata.decode.sin = metadata.decode.sin.clone()\n return\n\n if isinstance(metadata, AscendDSAMetadata):\n # DSA exposes RoPE storage through RopeDataProxy rather than tensors.\n # Rebuild from the immutable full table instead of depending on the\n # proxy's private representation or its reusable runtime buffer.\n metadata.cos, metadata.sin = get_cos_and_sin_dsa(\n input_positions[: metadata.num_input_tokens].long(),\n use_cache=False,\n )\n if metadata.prefill is not None:\n metadata.prefill.cos, metadata.prefill.sin = get_cos_and_sin_dsa(\n metadata.prefill.input_positions,\n use_cache=False,\n )\n if metadata.decode is not None:\n metadata.decode.cos, metadata.decode.sin = get_cos_and_sin_dsa(\n metadata.decode.input_positions,\n use_cache=False,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/npu/deepseek_attention_metadata.py:L108-L120", + "text": "def materialize_deepseek_attention_metadata_by_layer(\n metadata_by_layer: Mapping[str, AttentionMetadata],\n input_positions: torch.Tensor,\n) -> None:\n \"\"\"Materialize each shared attention-group metadata object exactly once.\"\"\"\n\n materialized_ids: set[int] = set()\n for metadata in metadata_by_layer.values():\n metadata_id = id(metadata)\n if metadata_id in materialized_ids:\n continue\n materialize_deepseek_attention_metadata(metadata, input_positions)\n materialized_ids.add(metadata_id)" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "id": "init-good-deepen-AFD-I30", + "note": "kb init deepen: judge said pass; on main the text is kept", + "rule_id": "AFD-I30", + "source": "owner", + "stage": "deepen", + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I34.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I34.json new file mode 100644 index 00000000..420c7cac --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I34.json @@ -0,0 +1,42 @@ +{ + "base": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_metadata.py:L197-L224", + "text": " if forward_context.additional_kwargs is None:\n forward_context.additional_kwargs = {}\n existing_metadata: AFDForwardContextMetadata | None = (\n forward_context.additional_kwargs.get(\"afd_metadata\")\n )\n if existing_metadata is not None and _is_ubatch_child_afd_context(\n forward_context,\n existing_metadata,\n ):\n return\n\n if self._afd_pending_metadata is None:\n self._afd_pending_metadata = self.build_afd_metadata(\n forward_context.ubatch_slices,\n self._afd_num_tokens_for_context(forward_context),\n )\n if self._afd_pending_metadata is not None:\n forward_context.additional_kwargs[\"afd_metadata\"] = (\n self._afd_pending_metadata\n )\n if getattr(self, \"_afd_suppress_metadata_send\", False):\n return\n dp_metadata = forward_context.dp_metadata\n ubatch_slices = forward_context.ubatch_slices\n padded_graph_tokens = _full_cudagraph_padded_tokens(forward_context)\n if padded_graph_tokens is not None and not ubatch_slices:\n dp_metadata = self.build_capture_dp_metadata(padded_graph_tokens)\n self.send_dp_metadata(dp_metadata, ubatch_slices)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_metadata.py:L237-L245", + "text": "def _is_ubatch_child_afd_context(\n forward_context: ForwardContext,\n afd_metadata: AFDForwardContextMetadata,\n) -> bool:\n if forward_context.ubatch_slices is not None:\n return False\n if int(afd_metadata.num_stages or 1) <= 1:\n return False\n return len(afd_metadata.tokens_lens or []) == 1" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_metadata.py:L52-L63", + "text": " if ubatch_slices and len(ubatch_slices) > 1:\n tokens_start_loc = [ub.token_slice.start for ub in ubatch_slices]\n requests_start_loc = [ub.request_slice.start for ub in ubatch_slices]\n tokens_lens = [ub.num_tokens for ub in ubatch_slices]\n tokens_unpadded_lens = [int(ub.num_tokens) for ub in ubatch_slices]\n num_stages = len(ubatch_slices)\n else:\n tokens_start_loc = [0]\n requests_start_loc = [0]\n tokens_lens = [num_tokens_unpadded]\n tokens_unpadded_lens = [num_tokens_unpadded]\n num_stages = 1" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L167-L170", + "text": " self._afd_pending_metadata = self.build_afd_metadata(\n ubatch_slices,\n int(num_tokens),\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L493-L499", + "text": " if allow_microbatching:\n # The AFD-aware ubatch wrapper builds the exact padded ubatch\n # slices used by vLLM and sends per-stage DP metadata before it\n # enters torch.cuda.graph(...). Avoid sending a single-stage\n # capture payload here.\n self._afd_pending_metadata = None\n self._afd_suppress_metadata_send = False" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n" + }, + "id": "init-good-deepen-AFD-I34", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I34", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I36.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I36.json new file mode 100644 index 00000000..51400b3b --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I36.json @@ -0,0 +1,47 @@ +{ + "base": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_metadata.py:L31-L36", + "text": " connector: AFDConnectorBase\n vllm_config: VllmConfig\n _is_warmup: bool\n _afd_transaction_counter: int\n _afd_is_profile: bool = False\n _afd_pending_metadata: AFDForwardContextMetadata | None" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_metadata.py:L226-L229", + "text": " def _next_afd_transaction_id(self) -> str:\n counter = self._afd_transaction_counter\n self._afd_transaction_counter = counter + 1\n return f\"afd-{counter}\"" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L82-L90", + "text": " assert self.connector.control_plane is not None, (\n \"GPU model runner only supports control-plane-driven connectors\"\n )\n self._is_warmup = False\n self._afd_is_graph_capturing = False\n self._afd_is_graph_replaying = False\n self._afd_pending_metadata: AFDForwardContextMetadata | None = None\n self._afd_suppress_metadata_send = False\n self._afd_transaction_counter = 0" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L295-L304", + "text": " if connector.control_plane is None:\n raise RuntimeError(\n \"AFD ModelRunnerV2 requires a control-plane-driven connector\",\n )\n self._is_warmup = False\n self._afd_is_graph_capturing = False\n self._afd_is_graph_replaying = False\n self._afd_pending_metadata: AFDForwardContextMetadata | None = None\n self._afd_suppress_metadata_send = False\n self._afd_transaction_counter = 0" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L102-L107", + "text": "from afd_plugin.v1.worker.attention_metadata import (\n _forward_context_num_tokens,\n _full_cudagraph_padded_tokens,\n _resolve_world_ranks,\n build_ubatch_dp_metadata_list,\n)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L159-L166", + "text": " self._is_warmup = False\n self._afd_is_graph_capturing = False\n self._afd_is_graph_replaying = False\n self._afd_pending_metadata: AFDForwardContextMetadata | None = None\n self._afd_suppress_metadata_send = False\n self._afd_transaction_counter = 0\n self._afd_async_moe_ubatch_metadata: AsyncMoeUbatchMetadata | None = None\n self._afd_live_execution = False" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n" + }, + "id": "init-good-deepen-AFD-I36", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I36", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I37.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I37.json new file mode 100644 index 00000000..81f5f7a7 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I37.json @@ -0,0 +1,42 @@ +{ + "base": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L105-L116", + "text": " def load_model(self, load_dummy_weights: bool = False) -> None:\n use_ubatching = bool(self.vllm_config.parallel_config.use_ubatching)\n with _use_afd_ubatch_wrapper_during_load(use_ubatching):\n super().load_model(load_dummy_weights)\n if use_ubatching:\n self._install_afd_ubatch_wrapper()\n # Wrapper installation is local and non-blocking; the connector\n # rendezvous is the blocking cross-role collective, so it is\n # deliberately last: it doubles as the \"both roles finished loading\n # weights\" barrier before memory profiling.\n if not self.connector.is_initialized:\n self.connector.init_afd_connector()" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L539-L545", + "text": " def shutdown(self) -> None:\n # ### PATCH START: extend native shutdown with AFD resource cleanup.\n stop_afd_gpu_profiler(self.prof)\n try:\n super().shutdown()\n finally:\n self.connector.close()" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L306-L312", + "text": " except BaseException:\n try:\n if connector is not None:\n connector.close()\n finally:\n super().shutdown()\n raise" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L338-L341", + "text": " def load_model(self, load_dummy_weights: bool = False) -> None:\n super().load_model(load_dummy_weights)\n if not self.connector.is_initialized:\n self.connector.init_afd_connector()" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L442-L451", + "text": " def shutdown(self) -> None:\n # ### PATCH START: guarantee profiler/native/connector cleanup.\n try:\n stop_afd_gpu_profiler(self.prof)\n finally:\n try:\n super().shutdown()\n finally:\n self._afd_pending_metadata = None\n self.connector.close()" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n" + }, + "id": "init-good-deepen-AFD-I37", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I37", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I38.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I38.json new file mode 100644 index 00000000..016045dc --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I38.json @@ -0,0 +1,37 @@ +{ + "base": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L118-L136", + "text": " def _install_afd_ubatch_wrapper(self) -> None:\n if isinstance(self.model, AFDUBatchWrapper):\n self.model.configure_afd_context_provider(\n self.install_afd_metadata_on_forward_context,\n )\n return\n\n model = self.model\n if isinstance(model, UBatchWrapper):\n model = model.unwrap()\n self.model = AFDUBatchWrapper(\n model,\n self.vllm_config,\n CUDAGraphMode.NONE,\n self.device,\n )\n self.model.configure_afd_context_provider(\n self.install_afd_metadata_on_forward_context,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L588-L599", + "text": "@contextmanager\ndef _use_afd_ubatch_wrapper_during_load(enabled: bool):\n if not enabled:\n yield\n return\n\n original = gpu_model_runner.UBatchWrapper\n gpu_model_runner.UBatchWrapper = AFDUBatchWrapper\n try:\n yield\n finally:\n gpu_model_runner.UBatchWrapper = original" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_worker.py:L85-L97", + "text": " afd_model_config = get_afd_model_config(\n self.vllm_config.model_config,\n device_type=\"cuda\",\n )\n self.vllm_config.model_config = afd_model_config\n self.model_config = afd_model_config\n\n native_runner_cls = native_runner_module.GPUModelRunner\n native_runner_module.GPUModelRunner = afd_runner_cls\n try:\n super().init_device()\n finally:\n native_runner_module.GPUModelRunner = native_runner_cls" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L139-L143", + "text": " v2_cudagraph_utils.prepare_inputs_to_capture = prepare_inputs_to_capture\n try:\n yield\n finally:\n v2_cudagraph_utils.prepare_inputs_to_capture = original_prepare" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n" + }, + "id": "init-good-deepen-AFD-I38", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I38", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I39.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I39.json new file mode 100644 index 00000000..457f130c --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I39.json @@ -0,0 +1,37 @@ +{ + "base": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L239-L277", + "text": " self._afd_is_graph_replaying = (\n not bool(getattr(self, \"_is_warmup\", False))\n and not bool(getattr(self, \"_afd_is_graph_capturing\", False))\n and cudagraph_mode == CUDAGraphMode.FULL\n )\n\n args = (\n num_tokens,\n num_reqs,\n num_scheduled_tokens_np,\n max_num_scheduled_tokens,\n use_cascade_attn,\n allow_microbatching,\n force_eager,\n force_uniform_decode,\n force_has_lora,\n force_num_active_loras,\n num_encoder_reqs,\n )\n kwargs: dict[str, Any] = {}\n\n # determine if ubatch should be activated.\n # 1. For dp = 1, vLLM hardcodes `should_ubatch=False`.\n # This is the extra support for dp = 1\n if self.vllm_config.parallel_config.data_parallel_size == 1:\n should_ubatch = self._should_ubatch_single_rank(\n batch_descriptor,\n args,\n kwargs,\n )\n\n # 2. For dp > 1, vLLM's coordinated decision (_post_process_ubatch)\n # only aborts when the last ubatch is empty. This ensures the first\n # ubatch is not empty\n elif should_ubatch:\n values = _batch_execution_values(args, kwargs)\n num_ubatches = self.vllm_config.parallel_config.num_ubatches\n num_tokens = int(values.get(\"num_tokens\", 0))\n should_ubatch = num_tokens >= max(num_ubatches, 1)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L305-L331", + "text": " parallel_config = self.vllm_config.parallel_config\n if not bool(parallel_config.use_ubatching):\n return False\n values = _batch_execution_values(args, kwargs)\n if not bool(values.get(\"allow_microbatching\", True)):\n return False\n num_tokens = values[\"num_tokens\"]\n num_ubatches = parallel_config.num_ubatches\n # Not covered by is_last_ubatch_empty: with no cudagraph padding,\n # fewer tokens than ubatches empties the *first* ubatch instead.\n if num_tokens < max(num_ubatches, 1):\n return False\n uniform_decode = self._is_uniform_decode(\n max_num_scheduled_tokens=values[\"max_num_scheduled_tokens\"],\n uniform_decode_query_len=self.uniform_decode_query_len,\n num_tokens=num_tokens,\n num_reqs=values[\"num_reqs\"],\n force_uniform_decode=values.get(\"force_uniform_decode\"),\n )\n if not check_ubatch_thresholds(\n parallel_config,\n num_tokens,\n bool(uniform_decode),\n ):\n return False\n padded_tokens = batch_descriptor.num_tokens\n return not is_last_ubatch_empty(num_tokens, padded_tokens, num_ubatches)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L549-L556", + "text": "def fail_if_unsupported_ubatching(vllm_config: VllmConfig) -> None:\n parallel_config = vllm_config.parallel_config\n num_ubatches = int(parallel_config.num_ubatches)\n if bool(vllm_config.parallel_config.use_ubatching) and num_ubatches != 2:\n raise RuntimeError(\n \"AFD ubatching currently supports exactly two ubatches; \"\n f\"got num_ubatches={num_ubatches}\",\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L566-L585", + "text": "def _batch_execution_values(\n args: tuple[Any, ...],\n kwargs: dict[str, Any],\n) -> dict[str, Any]:\n names = [\n \"num_tokens\",\n \"num_reqs\",\n \"num_scheduled_tokens_np\",\n \"max_num_scheduled_tokens\",\n \"use_cascade_attn\",\n \"allow_microbatching\",\n \"force_eager\",\n \"force_uniform_decode\",\n \"force_has_lora\",\n \"force_num_active_loras\",\n \"num_encoder_reqs\",\n ]\n values = dict(zip(names, args, strict=False))\n values.update(kwargs)\n return values" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n" + }, + "id": "init-good-deepen-AFD-I39", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I39", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I40.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I40.json new file mode 100644 index 00000000..8a85f473 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I40.json @@ -0,0 +1,22 @@ +{ + "base": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L138-L197", + "text": " # Patch reason: AFD stages connector metadata before native Attention\n # metadata construction. In addition, vLLM v0.26 caches Attention metadata\n # without the ubatch id, so update-capable backends can reuse ubatch 0\n # sequence metadata for later ubatches.\n # Patch functionality: stage the AFD metadata and disable block-table-only\n # metadata updates while native multi-ubatch metadata is built. Remove the\n # cache workaround after vLLM PR #48659 is included in the pinned release.\n # Signature: matches upstream; no added parameters.\n # Upstream: vLLM v0.26.0, vllm/v1/worker/gpu_model_runner.py\n # Commit: 568afb3a13806beb53bb2e6bd518269357b237c0\n # Delegation exception: the upstream function is large; this override wraps\n # the native implementation and scopes the workaround to its metadata-build\n # window.\n def _build_attention_metadata(\n self,\n num_tokens: int,\n num_reqs: int,\n max_query_len: int,\n num_tokens_padded: int | None = None,\n num_reqs_padded: int | None = None,\n ubatch_slices: UBatchSlices | None = None,\n logits_indices: torch.Tensor | None = None,\n use_spec_decode: bool = False,\n for_cudagraph_capture: bool = False,\n num_scheduled_tokens: dict[str, int] | None = None,\n cascade_attn_prefix_lens: list[list[int]] | None = None,\n slot_mappings: dict[int, torch.Tensor] | None = None,\n ) -> tuple[PerLayerAttnMetadata, CommonAttentionMetadata | None]:\n # ### PATCH START: stage AFD metadata and avoid cross-ubatch cache reuse.\n self._afd_pending_metadata = self.build_afd_metadata(\n ubatch_slices,\n int(num_tokens),\n )\n disabled_metadata_builders: dict[int, AttentionMetadataBuilder] = {}\n try:\n if ubatch_slices is not None and len(ubatch_slices) > 1:\n for kv_cache_group in self.attn_groups:\n for attention_group in kv_cache_group:\n for ubatch_idx in range(len(ubatch_slices)):\n builder = attention_group.get_metadata_builder(ubatch_idx)\n if builder.supports_update_block_table:\n disabled_metadata_builders[id(builder)] = builder\n builder.supports_update_block_table = False\n return super()._build_attention_metadata(\n num_tokens,\n num_reqs,\n max_query_len,\n num_tokens_padded,\n num_reqs_padded,\n ubatch_slices,\n logits_indices,\n use_spec_decode,\n for_cudagraph_capture,\n num_scheduled_tokens,\n cascade_attn_prefix_lens,\n slot_mappings,\n )\n finally:\n for builder in disabled_metadata_builders.values():\n builder.supports_update_block_table = True" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n" + }, + "id": "init-good-deepen-AFD-I40", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I40", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I42.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I42.json new file mode 100644 index 00000000..0fff7ad6 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I42.json @@ -0,0 +1,32 @@ +{ + "base": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L153-L216", + "text": " manager = runner.cudagraph_manager\n if manager is None:\n raise RuntimeError(\n \"AFD FULL graph replay hook requires an initialized graph manager\",\n )\n manager_state = vars(manager)\n if _AFD_FULLGRAPH_HOOK_MARKER in manager_state:\n raise RuntimeError(\"AFD FULL graph replay hook is already active\")\n\n had_instance_override = \"run_fullgraph\" in manager_state\n previous_instance_override = manager_state.get(\"run_fullgraph\")\n original_run_fullgraph = manager.run_fullgraph\n manager_state[_AFD_FULLGRAPH_HOOK_MARKER] = True\n\n # Patch reason: native FULL replay bypasses ForwardContext creation, so the\n # execute-scoped AFD provider cannot publish runtime control.\n # Patch functionality: wrap only this manager instance and publish one\n # ordinary padded control payload immediately before each native replay.\n # Signature: matches vLLM v0.26.0 ModelCudaGraphManager.run_fullgraph exactly.\n # Upstream source: vllm/v1/worker/gpu/cudagraph_utils.py,\n # ModelCudaGraphManager.run_fullgraph; commit\n # 568afb3a13806beb53bb2e6bd518269357b237c0.\n # Delegation exception: native replay remains wholly in the saved bound\n # method; this scope owns only the AFD pre-replay control seam.\n # Removal/upstream plan: delete this hook when vLLM exposes a per-manager\n # callback immediately before FULL graph replay.\n def run_fullgraph(\n self: v2_cudagraph_utils.ModelCudaGraphManager,\n desc: v2_cudagraph_utils.BatchExecutionDescriptor,\n ) -> torch.Tensor | tuple[torch.Tensor, list[torch.Tensor]] | IntermediateTensors:\n # ### PATCH START: publish one AFD pre-replay payload.\n previous_is_graph_replaying = getattr(\n runner,\n \"_afd_is_graph_replaying\",\n False,\n )\n try:\n padded_tokens = int(desc.num_tokens)\n metadata = runner.build_afd_metadata(None, real_tokens)\n metadata.tokens_lens = [padded_tokens]\n runner._afd_pending_metadata = metadata\n runner._afd_suppress_metadata_send = True\n runner._is_warmup = False\n runner._afd_is_graph_capturing = False\n runner._afd_is_graph_replaying = True\n runner.send_dp_metadata(\n runner.build_capture_dp_metadata(padded_tokens),\n None,\n )\n result = original_run_fullgraph(desc)\n finally:\n runner._afd_is_graph_replaying = previous_is_graph_replaying\n # ### PATCH END: publish one AFD pre-replay payload.\n return result\n\n try:\n manager.run_fullgraph = MethodType(run_fullgraph, manager)\n yield\n finally:\n if had_instance_override:\n manager.run_fullgraph = previous_instance_override\n else:\n del manager.run_fullgraph\n del manager_state[_AFD_FULLGRAPH_HOOK_MARKER]" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L226-L256", + "text": " use_fullgraph_replay_hook = (\n runner.vllm_config.compilation_config.cudagraph_mode\n == CUDAGraphMode.FULL_DECODE_ONLY\n and runner.cudagraph_manager is not None\n )\n previous_metadata = runner._afd_pending_metadata\n previous_suppress_send = runner._afd_suppress_metadata_send\n previous_is_warmup = runner._is_warmup\n previous_is_graph_capturing = runner._afd_is_graph_capturing\n previous_is_graph_replaying = getattr(runner, \"_afd_is_graph_replaying\", False)\n runner._afd_is_graph_replaying = False\n\n replay_scope = (\n _use_afd_fullgraph_replay_hook(runner, real_tokens)\n if use_fullgraph_replay_hook\n else nullcontext()\n )\n try:\n with (\n replay_scope,\n use_afd_metadata_provider(\n runner.install_afd_metadata_on_forward_context,\n ),\n ):\n yield\n finally:\n runner._afd_pending_metadata = previous_metadata\n runner._afd_suppress_metadata_send = previous_suppress_send\n runner._is_warmup = previous_is_warmup\n runner._afd_is_graph_capturing = previous_is_graph_capturing\n runner._afd_is_graph_replaying = previous_is_graph_replaying" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L417-L420", + "text": " with _use_afd_execution_context(\n self,\n int(scheduler_output.total_num_scheduled_tokens),\n ):" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n" + }, + "id": "init-good-deepen-AFD-I42", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I42", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I43.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I43.json new file mode 100644 index 00000000..6a21602f --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I43.json @@ -0,0 +1,52 @@ +{ + "base": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L118-L121", + "text": "# Upstream backend initialization creates builder 0 for full-batch metadata.\n# Async CAM extends that list, and stage ``ubid`` uses builder\n# ``ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid``.\nASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET = 1" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L286-L306", + "text": " if self.afd_async_extra_info.async_moe_ubatching:\n self.ubatch_slices = None\n return self._build_attention_metadata_with_async_moe_ubatches(\n num_tokens=num_tokens,\n num_reqs=num_reqs,\n max_query_len=max_query_len,\n num_tokens_padded=num_tokens_padded,\n num_reqs_padded=num_reqs_padded,\n ubatch_slices=ubatch_slices,\n logits_indices=logits_indices,\n use_spec_decode=use_spec_decode,\n for_cudagraph_capture=for_cudagraph_capture,\n num_scheduled_tokens=num_scheduled_tokens,\n num_scheduled_tokens_np=num_scheduled_tokens_np,\n cascade_attn_prefix_lens=cascade_attn_prefix_lens,\n )\n self._afd_pending_metadata = self._build_afd_metadata(\n ubatch_slices,\n num_tokens,\n )\n self.ubatch_slices = ubatch_slices" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L368-L372", + "text": " self._afd_async_moe_ubatch_metadata = None\n self._afd_pending_metadata = self._build_afd_metadata(\n None,\n num_tokens,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L491-L494", + "text": " # ### PATCH START: Reserve Async CAM stage builders\n metadata_builder_offset = (\n ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET if is_async_moe_stage_build else 0\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L620-L625", + "text": " attn_group = self.attn_groups[kv_cache_gid][attn_gid]\n # ### PATCH START: Async CAM builder offset\n builder = attn_group.get_metadata_builder(\n metadata_builder_offset + (ubid or 0),\n )\n # ### PATCH END: Async CAM builder offset" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L670-L676", + "text": " # ### PATCH START: Isolate Async CAM builder inputs\n if is_async_moe_stage_build:\n isolate_deepseek_attention_builder_inputs(\n builder,\n common_attn_metadata,\n )\n # ### PATCH END: Isolate Async CAM builder inputs" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L216-L217", + "text": " if self.ubatch_slices is not None:\n forward_context.ubatch_slices = self.ubatch_slices" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n" + }, + "id": "init-good-deepen-AFD-I43", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I43", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I45.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I45.json new file mode 100644 index 00000000..81e3f794 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I45.json @@ -0,0 +1,52 @@ +{ + "base": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L147-L148", + "text": " graph_key = make_ffn_graph_key(dp_metadata_list)\n cuda_graph_info = self._cuda_graphs.get(graph_key)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L306-L306", + "text": " graph_key = make_ffn_graph_key(dp_metadata_list)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L322-L326", + "text": " self._cuda_graphs[graph_key] = {\n \"graph\": cudagraph,\n \"input_hidden_states\": output,\n \"output\": output,\n }" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L223-L225", + "text": " graph_key = self._make_graph_key(dp_metadata_list)\n acl_graphs = self._acl_graphs\n graph_info = acl_graphs.get(graph_key)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L256-L265", + "text": " def _make_graph_key(\n self,\n dp_metadata_list: dict[int, DPMetadata | AFDDPMetadata],\n ) -> tuple:\n return make_ffn_graph_key(\n dp_metadata_list,\n attention_size=int(self.connector.attn_size),\n ffn_size=int(self.connector.ffn_size),\n fallback=int(self.max_num_tokens),\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L488-L494", + "text": " graph_key = self._make_graph_key(dp_metadata_list)\n if graph_key in self._acl_graphs:\n logger.debug(\n \"AFD NPU FFN ACL graph capture skipped for existing key=%s\",\n graph_key,\n )\n return" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L518-L521", + "text": " self._acl_graphs[graph_key] = {\n \"graph\": graph,\n \"output\": output,\n }" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n" + }, + "id": "init-good-deepen-AFD-I45", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I45", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I46.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I46.json new file mode 100644 index 00000000..33dfc12f --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I46.json @@ -0,0 +1,32 @@ +{ + "base": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L156-L158", + "text": " if run_mode is AFDGraphRunMode.REPLAY:\n cuda_graph_info[\"graph\"].replay()\n return None" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L234-L242", + "text": " if run_mode is AFDGraphRunMode.REPLAY:\n assert graph_info is not None\n logger.debug(\n \"AFD NPU FFN replaying ACL graph; key=%s cached_graphs=%d\",\n graph_key,\n len(acl_graphs),\n )\n graph_info[\"graph\"].replay()\n return None" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L322-L326", + "text": " self._cuda_graphs[graph_key] = {\n \"graph\": cudagraph,\n \"input_hidden_states\": output,\n \"output\": output,\n }" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n" + }, + "id": "init-good-deepen-AFD-I46", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I46", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I47.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I47.json new file mode 100644 index 00000000..328d5e79 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I47.json @@ -0,0 +1,27 @@ +{ + "base": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L178-L197", + "text": " if bool(self.use_aclgraph) and (is_graph_capturing or is_warmup):\n logger.debug(\n \"AFD NPU FFN execute_ffn_step enters capture_model; \"\n \"key=%s is_graph_capturing=%s is_warmup=%s\",\n self._make_graph_key(dp_metadata_list),\n is_graph_capturing,\n is_warmup,\n )\n self.capture_model(\n dp_metadata_list=dp_metadata_list,\n is_warmup=is_warmup,\n is_attn_graph_capturing=is_graph_capturing,\n )\n return None\n self.execute_model(\n dp_metadata_list=dp_metadata_list,\n is_profile=is_profile,\n is_graph_replaying=is_graph_replaying,\n )\n return None" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L226-L248", + "text": " graph_enabled = bool(self.use_aclgraph)\n run_mode = graph_run_mode(\n is_warmup=is_warmup and graph_enabled,\n is_graph_capturing=is_graph_capturing and graph_enabled,\n is_graph_replaying=is_graph_replaying,\n graph_enabled=graph_enabled,\n graph_exists=graph_info is not None,\n )\n if run_mode is AFDGraphRunMode.REPLAY:\n assert graph_info is not None\n logger.debug(\n \"AFD NPU FFN replaying ACL graph; key=%s cached_graphs=%d\",\n graph_key,\n len(acl_graphs),\n )\n graph_info[\"graph\"].replay()\n return None\n if run_mode in (AFDGraphRunMode.WARMUP, AFDGraphRunMode.CAPTURE):\n return self.execute_ffn_step(\n dp_metadata_list=dp_metadata_list,\n is_graph_capturing=is_graph_capturing,\n is_warmup=is_warmup,\n )" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n" + }, + "id": "init-good-deepen-AFD-I47", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I47", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I48.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I48.json new file mode 100644 index 00000000..5876f1c6 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I48.json @@ -0,0 +1,42 @@ +{ + "base": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L186-L217", + "text": " experts_layer_indices = frozenset(\n self.model.get_experts_layer_indices(),\n )\n layer_indices = (\n tuple(sorted(experts_layer_indices))\n if self.afd_config.compute_gate_on_attention\n else tuple(range(num_layers))\n )\n stage_ids = sorted(int(stage_idx) for stage_idx in dp_metadata_list) or [0]\n ffn_dp_metadata_list = {\n stage_idx: self._make_ffn_dp_metadata(dp_metadata_list[stage_idx])\n for stage_idx in stage_ids\n }\n recv_input_ids = getattr(self.model, \"afd_requires_input_ids\", False)\n with _ffn_forward_context(self.vllm_config) as forward_context:\n for layer_idx in layer_indices:\n uses_remote_experts = layer_idx in experts_layer_indices\n routing_spec = (\n self.model.get_experts_routing_spec(layer_idx)\n if uses_remote_experts and self.afd_config.compute_gate_on_attention\n else None\n )\n for stage_idx in stage_ids:\n recv_kwargs: dict[str, Any] = {}\n if routing_spec is not None:\n recv_kwargs[\"routing_spec\"] = routing_spec\n if recv_input_ids:\n recv_kwargs[\"recv_input_ids\"] = True\n payload = self.connector.recv_attn_output(\n ubatch_idx=stage_idx,\n **recv_kwargs,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L246-L246", + "text": " self.connector.send_ffn_output(rank_ffn_output, context)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L303-L304", + "text": " for layer_idx in _ffn_layer_indices(self):\n for stage_idx in stage_ids:" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L565-L586", + "text": "def _ffn_layer_indices(runner: AFDNPUFFNModelRunner) -> range | list[int]:\n num_layers = max(int(runner.num_layers or 0), 1)\n if not runner.afd_config.compute_gate_on_attention:\n return range(num_layers)\n hf_config = runner.model_config.hf_config\n return [\n layer_idx\n for layer_idx in range(num_layers)\n if _is_moe_layer(hf_config, layer_idx)\n ]\n\n\ndef _is_moe_layer(hf_config: object, layer_idx: int) -> bool:\n moe_layer_freq = getattr(hf_config, \"moe_layer_freq\", 1)\n # DeepSeek V4 has routed experts in every decoder layer and does not expose\n # the V2/V3 ``first_k_dense_replace`` compatibility field.\n first_moe_layer = getattr(hf_config, \"first_k_dense_replace\", 0)\n return (\n getattr(hf_config, \"n_routed_experts\", None) is not None\n and layer_idx >= first_moe_layer\n and layer_idx % moe_layer_freq == 0\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L132-L137", + "text": " if tuple(sorted(layer.layer_idx for layer in layers)) != tuple(\n _ffn_layer_indices(self)\n ):\n raise ValueError(\n \"layered GMM weights do not cover the remote MoE layer IDs\"\n )" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n" + }, + "id": "init-good-deepen-AFD-I48", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I48", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I50.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I50.json new file mode 100644 index 00000000..31144531 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I50.json @@ -0,0 +1,27 @@ +{ + "base": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_metadata.py:L17-L55", + "text": " \"\"\"Aggregate consecutive Attention-rank counts for each FFN rank.\n\n The blocks are the ones the rank mapping builds: contiguous and differing\n in size by at most one, so ``3A2F`` groups ``{A0, A1}`` onto ``F0`` and\n ``{A2}`` onto ``F1``. For example, ``4A2F`` counts ``(0, 4, 5, 6)`` become\n ``(5, 11)`` because every zero-token Attention peer contributes one\n placeholder row. Missing peers use the same per-peer fallback, so empty\n counts become ``(2, 2)``.\n \"\"\"\n\n fallback_count = max(1, int(fallback))\n fallback_counts = tuple(fallback_count for _ in range(max(0, ffn_size)))\n if ffn_size <= 0 or attention_size < ffn_size:\n return fallback_counts\n\n expanded_counts = attention_counts\n if (\n attention_counts\n and len(attention_counts) < attention_size\n and attention_size % len(attention_counts) == 0\n ):\n attention_ranks_per_count = attention_size // len(attention_counts)\n expanded_counts = tuple(\n attention_counts[rank // attention_ranks_per_count]\n for rank in range(attention_size)\n )\n\n # Each FFN rank sums the block that ``build_rank_mapping`` assigns to it.\n return tuple(\n sum(\n max(1, int(expanded_counts[attention_rank]))\n if attention_rank < len(expanded_counts)\n else fallback_count\n for attention_rank in subgroup_attention_block(\n ffn_rank, attention_size, ffn_size\n )\n )\n for ffn_rank in range(ffn_size)\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L602-L639", + "text": "def _ffn_token_counts_across_ranks(\n connector: AFDConnectorBase,\n dp_metadata_list: dict[int, DPMetadata | AFDDPMetadata],\n stage_idx: int,\n *,\n fallback: int,\n) -> torch.Tensor:\n dp_metadata = dp_metadata_list.get(int(stage_idx))\n if dp_metadata is None:\n values = [max(1, int(fallback))] * int(connector.ffn_size)\n else:\n attention_counts = _to_int_list(dp_metadata.num_tokens_across_dp_cpu)\n # Expand DP-level counts to AFD-level counts when TP > 1.\n # With TP, attn_size = num_attention_ranks includes TP workers\n # but num_tokens_across_dp_cpu only has dp_size entries.\n # Each DP rank's token count is replicated tp_size times because\n # all TP workers within the same DP rank process the same tokens.\n if (\n len(attention_counts) < int(connector.attn_size)\n and int(connector.attn_size) % len(attention_counts) == 0\n ):\n tp_size = int(connector.attn_size) // len(attention_counts)\n attention_counts = [\n attention_counts[i // tp_size] for i in range(int(connector.attn_size))\n ]\n if (\n len(attention_counts) >= int(connector.attn_size)\n and int(connector.attn_size) >= int(connector.ffn_size)\n and int(connector.attn_size) % int(connector.ffn_size) == 0\n ):\n group_size = int(connector.attn_size) // int(connector.ffn_size)\n values = [\n max(1, sum(attention_counts[idx * group_size : (idx + 1) * group_size]))\n for idx in range(int(connector.ffn_size))\n ]\n else:\n values = [max(1, int(fallback))] * int(connector.ffn_size)\n return torch.tensor(values, dtype=torch.int32, device=\"cpu\")" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n" + }, + "id": "init-good-deepen-AFD-I50", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I50", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I51.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I51.json new file mode 100644 index 00000000..2a2898a1 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I51.json @@ -0,0 +1,42 @@ +{ + "base": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_metadata.py:L32-L42", + "text": " expanded_counts = attention_counts\n if (\n attention_counts\n and len(attention_counts) < attention_size\n and attention_size % len(attention_counts) == 0\n ):\n attention_ranks_per_count = attention_size // len(attention_counts)\n expanded_counts = tuple(\n attention_counts[rank // attention_ranks_per_count]\n for rank in range(attention_size)\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_metadata.py:L58-L73", + "text": "def project_ffn_token_counts_to_dp(\n ffn_counts: tuple[int, ...],\n *,\n dp_size: int,\n) -> tuple[int, ...]:\n \"\"\"Project FFN role-rank counts to vLLM data-parallel rank counts.\n\n For example, two FFN TP ranks per DP rank project\n ``(8, 8, 13, 13)`` to ``(8, 13)``.\n \"\"\"\n\n if len(ffn_counts) == dp_size or dp_size <= 0 or len(ffn_counts) % dp_size != 0:\n return ffn_counts\n\n ffn_ranks_per_dp = len(ffn_counts) // dp_size\n return tuple(ffn_counts[rank * ffn_ranks_per_dp] for rank in range(dp_size))" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L264-L280", + "text": " def _make_ffn_dp_metadata(\n self,\n dp_metadata: DPMetadata | AFDDPMetadata,\n ) -> AFDDPMetadata:\n attention_counts = tuple(\n int(count) for count in dp_metadata.num_tokens_across_dp_cpu\n )\n ffn_counts = aggregate_ffn_token_counts(\n attention_counts,\n attention_size=int(self.connector.attn_size),\n ffn_size=int(self.connector.ffn_size),\n )\n dp_counts = project_ffn_token_counts_to_dp(\n ffn_counts,\n dp_size=int(self.vllm_config.parallel_config.data_parallel_size),\n )\n return AFDDPMetadata(num_tokens_across_dp_cpu=dp_counts)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L614-L626", + "text": " # Expand DP-level counts to AFD-level counts when TP > 1.\n # With TP, attn_size = num_attention_ranks includes TP workers\n # but num_tokens_across_dp_cpu only has dp_size entries.\n # Each DP rank's token count is replicated tp_size times because\n # all TP workers within the same DP rank process the same tokens.\n if (\n len(attention_counts) < int(connector.attn_size)\n and int(connector.attn_size) % len(attention_counts) == 0\n ):\n tp_size = int(connector.attn_size) // len(attention_counts)\n attention_counts = [\n attention_counts[i // tp_size] for i in range(int(connector.attn_size))\n ]" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L666-L686", + "text": "def _to_dp_level_token_counts(\n num_tokens_across_dp: torch.Tensor,\n *,\n dp_size: int,\n) -> torch.Tensor:\n \"\"\"Project AFD-level token counts back to DP-level for vLLM's forward context.\n\n ``num_tokens_across_dp`` has ``ffn_size`` entries (one per AFD role_rank).\n With TP > 1 each DP rank's count is replicated ``tp_size`` times, so the\n layout is ``[dp0, dp0, dp1, dp1, ...]``. vLLM's ``DPMetadata.make()``\n expects exactly ``dp_size`` entries where ``[dp_rank] == batchsize``.\n \"\"\"\n numel = len(num_tokens_across_dp)\n if numel == dp_size:\n return num_tokens_across_dp\n if dp_size <= 0 or numel % dp_size != 0:\n return num_tokens_across_dp\n tp_size = numel // dp_size\n # Take the first TP slot of each DP group (all TP slots are identical).\n indices = [dp_idx * tp_size for dp_idx in range(dp_size)]\n return num_tokens_across_dp[indices].contiguous()" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n" + }, + "id": "init-good-deepen-AFD-I51", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I51", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I54.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I54.json new file mode 100644 index 00000000..4bc5dd37 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I54.json @@ -0,0 +1,27 @@ +{ + "base": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_worker.py:L80-L111", + "text": " if self.use_v2_model_runner:\n # ### PATCH START: construct the FFN runner through the V2 seam.\n validate_gpu_model_runner_v2_config(\n self.vllm_config,\n expected_role=\"ffn\",\n device_type=self.device_config.device_type,\n )\n afd_model_config = get_afd_model_config(\n self.vllm_config.model_config,\n device_type=\"cuda\",\n )\n self.vllm_config.model_config = afd_model_config\n self.model_config = afd_model_config\n\n native_runner_cls = gpu_model_runner_v2.GPUModelRunner\n gpu_model_runner_v2.GPUModelRunner = GPUFFNModelRunner\n try:\n super().init_device()\n finally:\n gpu_model_runner_v2.GPUModelRunner = native_runner_cls\n torch.accelerator.empty_cache()\n return\n # ### PATCH END: construct the FFN runner through the V2 seam.\n\n fail_if_unsupported_ubatching(self.vllm_config)\n\n super().init_device()\n self.vllm_config.model_config = get_afd_model_config(\n self.vllm_config.model_config,\n device_type=\"cuda\",\n )\n self.model_runner = GPUFFNModelRunner(self.vllm_config, self.device)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L64-L64", + "text": " def __init__(self, vllm_config: VllmConfig, device: object) -> None:" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n" + }, + "id": "init-good-deepen-AFD-I54", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I54", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I55.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I55.json new file mode 100644 index 00000000..0f968334 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I55.json @@ -0,0 +1,52 @@ +{ + "base": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L84-L86", + "text": " assert self.connector.control_plane is not None, (\n \"GPU model runner only supports control-plane-driven connectors\"\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_worker.py:L181-L189", + "text": " while not event.is_set():\n if self.model_runner.connector.control_plane is None:\n raise NotImplementedError(\n \"GPU FFN only supports control-plane-driven connectors; \"\n \"connectors without a control plane (control_plane is None) \"\n \"are not supported.\",\n )\n\n payload = self.model_runner.connector.control_plane.recv_dp_metadata_list()" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_worker.py:L170-L189", + "text": " while not event.is_set():\n if self.model_runner.connector.control_plane is None:\n self.model_runner.execute_connector_driven_step()\n torch.npu.synchronize()\n continue\n\n payload = self.model_runner.connector.control_plane.recv_dp_metadata_list()\n dp_metadata_list = payload.dp_metadata_list\n is_attn_graph_capturing = payload.is_graph_capturing\n is_warmup = payload.is_warmup\n is_profile = payload.is_profile\n is_graph_replaying = payload.is_graph_replaying\n\n self.model_runner.execute_ffn_step(\n dp_metadata_list=dp_metadata_list,\n is_graph_capturing=is_attn_graph_capturing,\n is_warmup=is_warmup,\n is_profile=is_profile,\n is_graph_replaying=is_graph_replaying,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L199-L207", + "text": " def execute_connector_driven_step(self) -> None:\n if self.connector.control_plane is not None:\n raise RuntimeError(\n \"execute_connector_driven_step requires a connector-driven \"\n \"AFD connector\",\n )\n step_afd_npu_profiler(self.prof)\n self._ffn_forward_connector_driven()\n return None" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L276-L286", + "text": " if update_connector_state:\n assert self.connector.control_plane, (\n \"Only DP metadata control plane supports \",\n \"update_connector_state == True.\",\n )\n self.connector.control_plane.update_state_from_dp_metadata(\n _make_dp_metadata_payload(\n dp_metadata_list,\n is_graph_capturing=is_graph_capturing,\n ),\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L372-L372", + "text": " connector = cast(CAMAsyncAFDConnector, self.connector)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L485-L487", + "text": " assert self.connector.control_plane, (\n \"Only DP metadata control plane supports graph capturing.\"\n )" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n" + }, + "id": "init-good-deepen-AFD-I55", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I55", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I58.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I58.json new file mode 100644 index 00000000..142c6f31 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I58.json @@ -0,0 +1,37 @@ +{ + "base": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/async_cam.py:L348-L382", + "text": " recv_output = self.recv_attn_output(\n stage_idx=stage_idx,\n layer_idx=0,\n batch_size=max(1, self.max_num_batched_tokens or max_num_tokens),\n ubatch_idx=stage_idx,\n )\n context = recv_output.context\n metadata = context.metadata\n states = _require_async_transfer_state(context)\n token_nums_rankid_layeridx = states.token_nums_rankid_layeridx\n if token_nums_rankid_layeridx is None:\n raise RuntimeError(\n \"AFD async CAM FFN work item requires \"\n \"TokenNums_Rankid_Layeridx from async_dispatch_recv\",\n )\n expert_token_nums = states.group_list\n assert expert_token_nums is not None\n # Preserve the full compact metadata for combine-send. Its total is\n # rank-wide, while counts describe only this receive chunk.\n control = torch.stack(\n (\n token_nums_rankid_layeridx[0],\n token_nums_rankid_layeridx[2],\n expert_token_nums.sum(dtype=torch.int64),\n )\n )\n total_num_tokens, layer_idx, num_tokens = control.cpu().tolist()\n\n metadata.layer_idx = layer_idx\n metadata.stage_idx = stage_idx\n metadata.seq_lens = [num_tokens]\n\n hidden_states = recv_output.hidden_states[:num_tokens]\n if states.dynamic_scales is not None:\n states.dynamic_scales = states.dynamic_scales[:num_tokens]" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/async_cam.py:L708-L719", + "text": " hidden_states, dynamic_scales, batch_info, expert_token_nums = outputs\n _log_cam_op_values(\n \"async_dispatch_recv\",\n \"outputs\",\n hidden_states=hidden_states,\n dynamic_scales=dynamic_scales,\n batch_info=batch_info,\n expert_token_nums=expert_token_nums,\n )\n states.token_nums_rankid_layeridx = batch_info\n states.group_list = expert_token_nums\n states.dynamic_scales = dynamic_scales" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/async_cam.py:L737-L745", + "text": " states = _require_async_transfer_state(context)\n token_nums_rankid_layeridx = states.token_nums_rankid_layeridx\n if token_nums_rankid_layeridx is None:\n token_nums_rankid_layeridx = kwargs.get(\"token_nums_rankid_layeridx\")\n if token_nums_rankid_layeridx is None:\n raise RuntimeError(\n \"AFD async CAM combine send requires \"\n \"TokenNums_Rankid_Layeridx from async_dispatch_recv\",\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/async_cam.py:L766-L770", + "text": " torch.ops.afd_ascend.afd_async_combine_send(\n ffn_output,\n self.comm_args,\n token_nums_rankid_layeridx,\n self.comm_id," + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n" + }, + "id": "init-good-deepen-AFD-I58", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I58", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I59.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I59.json new file mode 100644 index 00000000..25c377e1 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I59.json @@ -0,0 +1,22 @@ +{ + "base": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/async_cam.py:L394-L454", + "text": " def _send_ffn_output_payload(\n self,\n ffn_output: Tensor | AFDF2ATransferPayload,\n context: AFDTransferContext,\n *,\n stage_idx: int,\n ) -> None:\n if not isinstance(ffn_output, AFDF2ATransferPayload):\n self.send_ffn_output(\n ffn_output,\n context,\n ubatch_idx=stage_idx,\n )\n return\n\n self.send_ffn_output(\n ffn_output.routed_output,\n context,\n ubatch_idx=stage_idx,\n )\n\n def send_ffn_work_item_output(\n self,\n work_item: AFDAsyncFFNWorkItem,\n ffn_output: Tensor | AFDF2ATransferPayload,\n ) -> Tensor | AFDF2ATransferPayload:\n \"\"\"Return one FFN work item's routed output through CAM.\n\n A one-token floating routed placeholder is used when a rank receives no\n routed tokens because CAM combine-send cannot consume the dynamic\n quantized dispatch buffer as an empty routed result.\n \"\"\"\n if work_item.num_tokens > 0:\n self._send_ffn_output_payload(\n ffn_output,\n work_item.context,\n stage_idx=work_item.stage_idx,\n )\n return ffn_output\n\n # Temporary NPU workaround: CAM combine-send does not accept the int8\n # dispatch-recv buffer as routed output. When this FFN rank has no\n # routed tokens, send one fake floating routed token instead of reusing the\n # dynamicQuant int8 input buffer.\n fake_routed_output = torch.zeros(\n (1, work_item.recv_output.hidden_states.shape[-1]),\n dtype=self.activation_dtype,\n device=work_item.recv_output.hidden_states.device,\n )\n if isinstance(ffn_output, AFDF2ATransferPayload):\n ffn_output = AFDF2ATransferPayload(\n routed_output=fake_routed_output,\n )\n else:\n ffn_output = fake_routed_output\n self._send_ffn_output_payload(\n ffn_output,\n work_item.context,\n stage_idx=work_item.stage_idx,\n )\n return ffn_output" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n" + }, + "id": "init-good-deepen-AFD-I59", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I59", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I62.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I62.json new file mode 100644 index 00000000..e7c11ca8 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I62.json @@ -0,0 +1,37 @@ +{ + "base": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/async_cam.py:L71-L85", + "text": "ATTN_RANKS_PER_DP_CONFIG_KEY = \"attn_ranks_per_dp\"\nASYNC_MOE_NUM_STAGES = 2\nASYNC_MOE_REQUEST_SPLIT = \"request\"\nASYNC_MOE_TOKEN_SPLIT = \"token\"\n\n_AFD_ASYNC_EXTRA_CONFIG_FIELDS: Final[frozenset[str]] = frozenset(\n {\n \"dynamicQuant\",\n \"attn_ranks_per_dp\",\n \"async_moe_ubatching\",\n \"async_moe_num_ubatches\",\n \"async_moe_split\",\n \"hccl_buffer_size\",\n },\n)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/async_cam.py:L111-L166", + "text": " @classmethod\n def from_mapping(cls, raw: Mapping[str, Any] | None) -> AFDAsyncExtraInfo:\n if raw is None:\n raw = {}\n if not isinstance(raw, Mapping):\n raise TypeError(\n f\"{cls.__name__} connector_extra_config must be a mapping, \"\n f\"got {type(raw).__name__}\",\n )\n unknown = sorted(\n str(key) for key in raw if key not in _AFD_ASYNC_EXTRA_CONFIG_FIELDS\n )\n if unknown:\n raise ValueError(\n \"unknown AFD async connector_extra_config field(s): \"\n + \", \".join(unknown),\n )\n\n return cls(\n dynamic_quant=coerce_extra_int(\n raw.get(\"dynamicQuant\", 0),\n field_name=\"dynamicQuant\",\n ),\n attn_ranks_per_dp=coerce_extra_positive_int(\n raw.get(\"attn_ranks_per_dp\", 1),\n field_name=\"attn_ranks_per_dp\",\n ),\n async_moe_ubatching=coerce_extra_bool(\n raw.get(\"async_moe_ubatching\", False),\n field_name=\"async_moe_ubatching\",\n ),\n async_moe_num_ubatches=coerce_extra_positive_int(\n raw.get(\"async_moe_num_ubatches\", ASYNC_MOE_NUM_STAGES),\n field_name=\"async_moe_num_ubatches\",\n ),\n async_moe_split=coerce_extra_str(\n raw.get(\"async_moe_split\", ASYNC_MOE_REQUEST_SPLIT),\n field_name=\"async_moe_split\",\n ),\n hccl_buffer_size=coerce_optional_extra_positive_int(\n raw.get(\"hccl_buffer_size\"),\n field_name=\"hccl_buffer_size\",\n ),\n )\n\n def to_mapping(self) -> dict[str, Any]:\n result: dict[str, Any] = {\n \"dynamicQuant\": self.dynamic_quant,\n \"attn_ranks_per_dp\": self.attn_ranks_per_dp,\n \"async_moe_ubatching\": self.async_moe_ubatching,\n \"async_moe_num_ubatches\": self.async_moe_num_ubatches,\n \"async_moe_split\": self.async_moe_split,\n }\n if self.hccl_buffer_size is not None:\n result[\"hccl_buffer_size\"] = self.hccl_buffer_size\n return result" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L65-L74", + "text": "_CAMP2P_EXTRA_CONFIG_FIELDS: Final[frozenset[str]] = frozenset(\n {\n \"core_num\",\n \"attn_core_num\",\n \"ffn_core_num\",\n \"compute_gate_on_attention\",\n \"hccl_buffer_size\",\n \"quant_mode\",\n },\n)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L97-L168", + "text": " @classmethod\n def from_mapping(cls, raw: Mapping[str, Any] | None) -> CAMP2PExtraInfo:\n if raw is None:\n raw = {}\n if not isinstance(raw, Mapping):\n raise TypeError(\n f\"{cls.__name__} connector_extra_config must be a mapping, \"\n f\"got {type(raw).__name__}\",\n )\n unknown = sorted(\n str(key) for key in raw if key not in _CAMP2P_EXTRA_CONFIG_FIELDS\n )\n if unknown:\n raise ValueError(\n \"unknown CAMP2P connector_extra_config field(s): \" + \", \".join(unknown),\n )\n\n return cls(\n core_num=coerce_extra_positive_int(\n raw.get(\"core_num\", 8),\n field_name=\"core_num\",\n ),\n attn_core_num=coerce_optional_extra_positive_int(\n raw.get(\"attn_core_num\"),\n field_name=\"attn_core_num\",\n ),\n ffn_core_num=coerce_optional_extra_positive_int(\n raw.get(\"ffn_core_num\"),\n field_name=\"ffn_core_num\",\n ),\n compute_gate_on_attention=coerce_extra_bool(\n raw.get(\"compute_gate_on_attention\", False),\n field_name=\"compute_gate_on_attention\",\n ),\n hccl_buffer_size=coerce_optional_extra_positive_int(\n raw.get(\"hccl_buffer_size\"),\n field_name=\"hccl_buffer_size\",\n ),\n quant_mode=coerce_extra_int(\n raw.get(\"quant_mode\", 0),\n field_name=\"quant_mode\",\n ),\n )\n\n def aiv_num_for_role(self, role: str) -> int:\n if role == \"attention\" and self.attn_core_num is not None:\n return self.attn_core_num\n if role == \"ffn\" and self.ffn_core_num is not None:\n return self.ffn_core_num\n return self.core_num\n\n def validate_supported(self) -> None:\n if self.compute_gate_on_attention:\n raise RuntimeError(\n \"AFD NPU runtime does not support compute_gate_on_attention=true yet\",\n )\n if self.quant_mode != 0:\n raise RuntimeError(\"AFD NPU runtime currently supports only quant_mode=0\")\n\n def to_mapping(self) -> dict[str, Any]:\n result: dict[str, Any] = {\n \"core_num\": self.core_num,\n \"compute_gate_on_attention\": self.compute_gate_on_attention,\n \"quant_mode\": self.quant_mode,\n }\n if self.attn_core_num is not None:\n result[\"attn_core_num\"] = self.attn_core_num\n if self.ffn_core_num is not None:\n result[\"ffn_core_num\"] = self.ffn_core_num\n if self.hccl_buffer_size is not None:\n result[\"hccl_buffer_size\"] = self.hccl_buffer_size\n return result" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n" + }, + "id": "init-good-deepen-AFD-I62", + "note": "kb init deepen: judge said pass; on main the text is kept", + "rule_id": "AFD-I62", + "source": "owner", + "stage": "deepen", + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I63.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I63.json new file mode 100644 index 00000000..f22bbee0 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I63.json @@ -0,0 +1,32 @@ +{ + "base": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L415-L468", + "text": " num_ubatches = max(1, self.vllm_config.parallel_config.num_ubatches)\n self.afd_pg_list = []\n self.hccl_comm_name_list = []\n for ubatch_idx in range(num_ubatches):\n group_name = \"afd\" if ubatch_idx == 0 else f\"afd{ubatch_idx}\"\n afd_pg = init_afd_process_group(\n backend=\"hccl\",\n init_method=f\"tcp://{self.afd_config.host}:{self.afd_config.port}\",\n world_size=self.ffn_size + self.attn_size,\n rank=self.world_rank,\n group_name=group_name,\n timeout=timedelta(minutes=30),\n pg_options=create_hccl_process_group_options(\n self.hccl_buffer_size_mb,\n ),\n )\n self.afd_pg_list.append(afd_pg)\n backend = afd_pg._get_backend(torch.device(\"npu\"))\n self.hccl_comm_name_list.append(\n str(backend.get_hccl_comm_name(self.world_rank)),\n )\n self.afd_pg = self.afd_pg_list[0]\n self.hccl_comm_name = self.hccl_comm_name_list[0]\n self.hccl_comm_name2 = (\n self.hccl_comm_name_list[1] if num_ubatches > 1 else self.hccl_comm_name\n )\n self.hccl_comm_name3 = self.hccl_comm_name_list[2] if num_ubatches > 2 else \"\"\n\n if self.afd_config.role == \"ffn\":\n self.ffn_pg = init_afd_process_group(\n backend=\"hccl\",\n init_method=f\"tcp://{self.afd_config.host}:{self.afd_config.port}\",\n world_size=self.ffn_size,\n rank=self.world_rank,\n group_name=\"afd_moe\",\n timeout=timedelta(minutes=30),\n pg_options=create_hccl_process_group_options(\n self.hccl_buffer_size_mb,\n ),\n )\n backend = self.ffn_pg._get_backend(torch.device(\"npu\"))\n self.hccl_comm_name1 = str(\n backend.get_hccl_comm_name(self.world_rank),\n )\n\n if self.topology.participates_in_p2p_group:\n self.p2p_pg = init_afd_process_group(\n backend=\"gloo\",\n init_method=f\"tcp://{self.afd_config.host}:{self.afd_config.port}\",\n world_size=self.topology.p2p_world_size,\n rank=self.p2p_rank,\n group_name=\"p2p\",\n timeout=timedelta(minutes=30),\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L699-L701", + "text": " custom_states.atten_batch_size = outputs[3]\n custom_states.x_active_mask = outputs[4]\n custom_states.cam_p2p_ep_name = self.hccl_comm_name1" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L939-L953", + "text": "def _get_group_ep(\n ubatch_idx: int,\n hccl_comm_name1: str,\n hccl_comm_name2: str,\n hccl_comm_name3: str,\n) -> str:\n if ubatch_idx == 1:\n return hccl_comm_name2 or hccl_comm_name1\n if ubatch_idx == 2:\n if not hccl_comm_name3:\n raise RuntimeError(\"CAMP2P ubatch 2 requires a third HCCL group\")\n return hccl_comm_name3\n if ubatch_idx < 0:\n raise RuntimeError(f\"CAMP2P ubatch index must be non-negative: {ubatch_idx}\")\n return hccl_comm_name1" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n" + }, + "id": "init-good-deepen-AFD-I63", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I63", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I64.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I64.json new file mode 100644 index 00000000..e9525f34 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I64.json @@ -0,0 +1,32 @@ +{ + "base": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L211-L224", + "text": " @property\n def p2p_world_size(self) -> int:\n \"\"\"Return the number of FFN and participating Attention metadata ranks.\"\"\"\n return self.ffn_size + self.min_size\n\n @property\n def participates_in_p2p_group(self) -> bool:\n \"\"\"Return whether this process joins the Gloo DP-metadata group.\"\"\"\n return self.world_rank < self.ffn_size or self.is_attn_top_min_size_rank\n\n @property\n def is_attn_top_min_size_rank(self) -> bool:\n \"\"\"Return whether this is an Attention metadata-sender rank.\"\"\"\n return self.ffn_size <= self.world_rank < self.ffn_size + self.min_size" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L789-L817", + "text": " def send_dp_metadata_list(\n self,\n payload: AFDControlPayload,\n ) -> None:\n connector = self.connector\n if connector.p2p_pg is None:\n return\n if not connector.topology.is_attn_top_min_size_rank:\n return\n # The CAMP2P DP-metadata group runs on gloo, so the wire tensors stay on\n # CPU rather than the NPU device.\n device = torch.device(\"cpu\")\n send_control_payload(\n payload,\n dst=connector.dst_list,\n group=connector.p2p_pg,\n device=device,\n )\n\n def recv_dp_metadata_list(self) -> AFDControlPayload:\n connector = self.connector\n if connector.p2p_pg is None:\n raise RuntimeError(\"CAMP2P metadata process group is not initialized\")\n src = connector.p2p_rank % connector.min_size + connector.ffn_size\n return recv_control_payload(\n src=src,\n group=connector.p2p_pg,\n device=torch.device(\"cpu\"),\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L840-L877", + "text": " attention_size, ffn_size = topology_from_config(afd_config)\n if attention_size <= 0 or ffn_size <= 0:\n raise ValueError(\"CAMP2P topology sizes must be positive\")\n if attention_size < ffn_size:\n raise ValueError(\n \"CAMP2P requires attention_size >= ffn_size, got \"\n f\"{attention_size} < {ffn_size}\",\n )\n if role_rank < 0:\n raise ValueError(f\"CAMP2P role rank must be non-negative, got {role_rank}\")\n\n if afd_config.role == \"attention\":\n if role_rank >= attention_size:\n raise ValueError(\n \"Attention role rank must be within attention size \"\n f\"(rank={role_rank}, size={attention_size})\",\n )\n world_rank = ffn_size + role_rank\n p2p_rank = role_rank + min(ffn_size, attention_size)\n elif afd_config.role == \"ffn\":\n if role_rank >= ffn_size:\n raise ValueError(\n \"FFN role rank must be within FFN size \"\n f\"(rank={role_rank}, size={ffn_size})\",\n )\n world_rank = role_rank\n p2p_rank = role_rank\n else:\n raise ValueError(f\"unknown AFD role {afd_config.role!r}\")\n\n min_size = min(attention_size, ffn_size)\n destinations: list[int] = []\n if ffn_size <= world_rank < ffn_size + min_size:\n local_attention_rank = world_rank - ffn_size\n dst = local_attention_rank\n while dst < ffn_size:\n destinations.append(dst)\n dst += min_size" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n" + }, + "id": "init-good-deepen-AFD-I64", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I64", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I65.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I65.json new file mode 100644 index 00000000..f58a9776 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I65.json @@ -0,0 +1,42 @@ +{ + "base": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L531-L575", + "text": " if not torch.compiler.is_compiling() and not metadata.validate_tensor_shape(\n tuple(hidden_states.shape),\n ):\n raise ValueError(\n f\"hidden_states shape {hidden_states.shape!r} does not match \"\n f\"CAMP2P metadata token count {metadata.total_tokens}\",\n )\n input_ids = cast(torch.Tensor | None, kwargs.get(\"input_ids\"))\n expert_ids: torch.Tensor | None = None\n expert_scales: torch.Tensor | None = None\n compute_gate = 0\n if input_ids is not None:\n expert_ids, expert_scales = prepare_token_id_transfer(\n input_ids,\n topk=self.num_experts_per_tok,\n expected_tokens=metadata.total_tokens,\n )\n compute_gate = 1\n transfer_state = CAMP2PTransferState(\n aiv_num=self.aiv_num,\n batch_size=metadata.total_tokens,\n h=self.hidden_size,\n k=self.num_experts_per_tok,\n )\n ubatch_idx = metadata.stage_idx\n forward_context = get_forward_context()\n forward_context.cam_afdtransfer_state = transfer_state\n forward_context.ubatch_idx = ubatch_idx\n\n torch.ops.vllm.afd_camp2p_send_attn_output(\n hidden_states,\n self.hccl_comm_name,\n self.hccl_comm_name2,\n self.hccl_comm_name3,\n transfer_state.batch_size,\n transfer_state.h,\n transfer_state.k,\n self.ffn_size,\n self.attn_size,\n self.world_rank,\n transfer_state.aiv_num,\n compute_gate,\n expert_ids,\n expert_scales,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L601-L617", + "text": " transfer_state = getattr(get_forward_context(), \"cam_afdtransfer_state\", None)\n if transfer_state is None:\n raise RuntimeError(\"CAMP2P Attention side is missing connector data\")\n get_forward_context().ubatch_idx = ubatch_idx\n return torch.ops.vllm.afd_camp2p_recv_ffn_output(\n ref_tensor,\n self.hccl_comm_name,\n self.hccl_comm_name2,\n self.hccl_comm_name3,\n transfer_state.batch_size,\n transfer_state.h,\n transfer_state.k,\n self.ffn_size,\n self.attn_size,\n self.world_rank,\n transfer_state.aiv_num,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L982-L1013", + "text": " transfer_state = getattr(get_forward_context(), \"cam_afdtransfer_state\", None)\n if transfer_state is None:\n transfer_state = CAMP2PTransferState()\n transfer_state.batch_size = batch_size\n transfer_state.h = hidden_size\n transfer_state.k = topk\n transfer_state.aiv_num = aiv_num\n group_ep = _get_group_ep(\n int(getattr(get_forward_context(), \"ubatch_idx\", 0)),\n hccl_comm_name,\n hccl_comm_name2,\n hccl_comm_name3,\n )\n\n outputs = torch.ops.afd_ascend.a2e(\n hidden_states,\n expert_ids,\n expert_scales,\n transfer_state.batch_size,\n transfer_state.h,\n transfer_state.k,\n ffn_size,\n attn_size,\n world_rank,\n group_ep,\n transfer_state.aiv_num,\n compute_gate,\n )\n transfer_state.atten_batch_size = outputs[3]\n forward_context = get_forward_context()\n forward_context.cam_afdtransfer_state = transfer_state\n return hidden_states" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L1047-L1071", + "text": " transfer_state = getattr(get_forward_context(), \"cam_afdtransfer_state\", None)\n if transfer_state is None or transfer_state.atten_batch_size is None:\n raise RuntimeError(\"CAMP2P Attention side is missing A2E handle data\")\n transfer_state.batch_size = batch_size\n transfer_state.h = hidden_size\n transfer_state.k = topk\n transfer_state.aiv_num = aiv_num\n group_ep = _get_group_ep(\n int(getattr(get_forward_context(), \"ubatch_idx\", 0)),\n hccl_comm_name,\n hccl_comm_name2,\n hccl_comm_name3,\n )\n output = torch.ops.afd_ascend.e2a(\n ref_tensor,\n transfer_state.atten_batch_size,\n transfer_state.batch_size,\n transfer_state.h,\n transfer_state.k,\n ffn_size,\n attn_size,\n world_rank,\n group_ep,\n transfer_state.aiv_num,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L1090-L1149", + "text": " send_annotations = {\n \"hidden_states\": torch.Tensor,\n \"hccl_comm_name\": str,\n \"hccl_comm_name2\": str,\n \"hccl_comm_name3\": str,\n \"batch_size\": int,\n \"hidden_size\": int,\n \"topk\": int,\n \"ffn_size\": int,\n \"attn_size\": int,\n \"world_rank\": int,\n \"aiv_num\": int,\n \"compute_gate\": int,\n \"expert_ids\": torch.Tensor | None,\n \"expert_scales\": torch.Tensor | None,\n \"return\": torch.Tensor,\n }\n recv_annotations = {\n \"ref_tensor\": torch.Tensor,\n \"hccl_comm_name\": str,\n \"hccl_comm_name2\": str,\n \"hccl_comm_name3\": str,\n \"batch_size\": int,\n \"hidden_size\": int,\n \"topk\": int,\n \"ffn_size\": int,\n \"attn_size\": int,\n \"world_rank\": int,\n \"aiv_num\": int,\n \"return\": torch.Tensor,\n }\n send_attn_output_impl.__annotations__ = send_annotations\n send_attn_output_fake_impl.__annotations__ = send_annotations\n recv_ffn_output_impl.__annotations__ = recv_annotations\n recv_ffn_output_fake_impl.__annotations__ = recv_annotations\n\n try:\n direct_register_custom_op(\n op_name=\"afd_camp2p_send_attn_output\",\n op_func=send_attn_output_impl,\n mutates_args=[],\n fake_impl=send_attn_output_fake_impl,\n dispatch_key=\"PrivateUse1\",\n )\n direct_register_custom_op(\n op_name=\"afd_camp2p_recv_ffn_output\",\n op_func=recv_ffn_output_impl,\n mutates_args=[],\n fake_impl=recv_ffn_output_fake_impl,\n dispatch_key=\"PrivateUse1\",\n )\n except RuntimeError as exc:\n message = str(exc).lower()\n duplicate = any(\n marker in message\n for marker in (\"already\", \"duplicate\", \"same name\", \"defined\")\n )\n if not duplicate:\n raise\n _CAMP2P_CUSTOM_OPS_REGISTERED = True" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n" + }, + "id": "init-good-deepen-AFD-I65", + "note": "kb init deepen: judge said unsure; on main the text is kept", + "rule_id": "AFD-I65", + "source": "owner", + "stage": "deepen", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I66.json b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I66.json new file mode 100644 index 00000000..caf54f4c --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-deepen-AFD-I66.json @@ -0,0 +1,42 @@ +{ + "base": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L253-L272", + "text": " num_tokens = int(input_ids.numel())\n if num_tokens != expected_tokens:\n raise ValueError(\n f\"input_ids token count {num_tokens} does not match the AFD \"\n f\"transfer token count {expected_tokens}\",\n )\n\n ids = (\n input_ids.reshape(-1)\n .to(dtype=torch.int32)\n .unsqueeze(1)\n .expand(-1, topk)\n .contiguous()\n )\n scales = torch.zeros(\n (num_tokens, topk),\n dtype=torch.float32,\n device=input_ids.device,\n )\n return ids, scales" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L300-L308", + "text": " received_tokens = int(sim_expert_ids.shape[0])\n if received_tokens < expected_tokens:\n raise ValueError(\n f\"received {received_tokens} token ids but the FFN rank computes on \"\n f\"{expected_tokens} tokens; the ids channel is not aligned with the \"\n \"FFN token layout\",\n )\n # The columns are replicas of the same id, so the first one carries it.\n return sim_expert_ids[:expected_tokens, 0].contiguous()" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L538-L548", + "text": " input_ids = cast(torch.Tensor | None, kwargs.get(\"input_ids\"))\n expert_ids: torch.Tensor | None = None\n expert_scales: torch.Tensor | None = None\n compute_gate = 0\n if input_ids is not None:\n expert_ids, expert_scales = prepare_token_id_transfer(\n input_ids,\n topk=self.num_experts_per_tok,\n expected_tokens=metadata.total_tokens,\n )\n compute_gate = 1" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L651-L654", + "text": " layer_idx: int = kwargs.get(\"layer_idx\", 0)\n max_num_tokens: int = kwargs.get(\"max_num_tokens\", 0)\n recv_input_ids: bool = bool(kwargs.get(\"recv_input_ids\", False))\n compute_gate_mode = 1 if recv_input_ids else 0" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L702-L719", + "text": " # The ids slot is only written in the operator's ids mode, so the mode has\n # to match the sending rank's ``compute_gate``. It is declared here by the\n # receiving FFN rank through ``recv_input_ids`` rather than inferred from\n # the returned tensor: a genuine single-token layer would otherwise be\n # indistinguishable from the operator's placeholder. Reading the slot in\n # the other mode would hand the model uninitialised device memory as token\n # ids, which a token-keyed router turns into an out-of-range table read.\n received_ids: torch.Tensor | None = None\n if compute_gate_mode == 1:\n received_ids = received_token_ids(\n outputs[1],\n expected_tokens=batch_size,\n )\n return AFDA2FTransferPayload(\n hidden_states=outputs[0],\n context=context,\n input_ids=received_ids,\n )" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n" + }, + "id": "init-good-deepen-AFD-I66", + "note": "kb init deepen: judge said pass; on main the text is kept", + "rule_id": "AFD-I66", + "source": "owner", + "stage": "deepen", + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I1.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I1.json new file mode 100644 index 00000000..b098182b --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I1.json @@ -0,0 +1,42 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L61-L63", + "text": "Connector implementations are grouped by backend package:\n`afd_plugin.connectors.gpu` for GPU-only connectors,\n`afd_plugin.connectors.npu` for NPU-only connectors." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L274-L281", + "text": "`role` must be `attention` or `ffn`. `connector` must be `P2pNcclAFDConnector`,\n`CAMP2pAFDConnector`, or `CAMAsyncAFDConnector`. AFD is active when\n`additional_config[\"afd\"]` is present and passes common AFD config validation;\nomit `additional_config[\"afd\"]` to disable AFD. Connector-owned\n`connector_extra_config` is strictly validated by the selected connector parser\nwhen the connector/runtime is constructed. The plugin also accepts selected\ncompatibility aliases such as `afd_role`, `afd_connector`, `afd_host`, and\n`afd_port`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L67-L85", + "text": "`AFDConnectorFactory` stores lazy loaders, so importing the factory does not\nimport CUDA or Ascend implementations. Built-in names are registered at module\nload time. `create_connector(rank, local_rank, vllm_config, afd_config=None)`\nparses configuration when needed, resolves the runtime role rank from the\nworker's global DP rank and local PCP/TP coordinates, and constructs the\nselected class. Every connector receives that resolved role rank and is only\nresponsible for mapping it into its backend-specific world rank. A loader must\nresolve to an `AFDConnectorBase` subclass; duplicate registration is rejected\nunless `replace=True`, and unknown names fail before resource initialization.\n\nThe registry method is a usable implementation hook, but the complete public\nextension contract is **draft**. Configuration still rejects names outside its\nseparate hard-coded allow-list, so factory registration alone is insufficient\nfor an external connector.\n\nEach connector class implements `parse_extra_config()` and construction stores\nthe resulting immutable `ConnectorExtraInfo` as `extra_info`. The factory can\nalso parse this information without constructing communication resources,\nwhich lets feature validation use the same connector-owned schema." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L332-L339", + "text": "The `AFDControlPlane` split removes control-plane methods from\n`AFDConnectorBase`, so CAM async no longer implements semantic no-ops. Its\nconnector-driven work-item methods still live outside the abstract base. The\nconfiguration allow-list is not derived from the factory registry, and\n`AFDA2FTransferPayload` combines common data with backend state.\nConnector-owned typed configuration implements the decision from\n[#89](https://github.com/JiusiServe/afd-plugin/issues/89), but it does not by\nitself make factory registration a public extension contract." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L135-L135", + "text": "| [`compat/npu/feature_validation.py`](../../../afd_plugin/compat/npu/feature_validation.py) | Parses connector-owned typed extra information through the factory and fails before execution for unsupported NPU connector, quantization, graph, DBO, gate, or async MoE combinations. |" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I1", + "note": "kb init skeleton: judge said unsure; on main the text is kept", + "rule_id": "AFD-I1", + "source": "owner", + "stage": "skeleton", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I10.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I10.json new file mode 100644 index 00000000..af7ff65a --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I10.json @@ -0,0 +1,67 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L145-L149", + "text": "| Surface | Caller and current responsibility |\n| --- | --- |\n| `update_state_from_dp_metadata(payload)` | Applies local stage shape/graph/warmup state to the owning connector without sending it. |\n| `send_dp_metadata_list(payload)` | Moves `AFDControlPayload` from Attention to FFN for control-plane-driven connectors. |\n| `recv_dp_metadata_list()` | Blocks on and returns the next control payload that drives an FFN step. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L163-L164", + "text": "| `AFDDPMetadata` | CPU token counts plus DP/SP-compatible sizing helpers. It adapts upstream-like metadata into a plugin-owned representation. | Wire representation is the candidate contract; helper surface is draft. |\n| `AFDControlPayload` | Stage-to-DP-metadata mapping plus graph-capture, warmup, and profile flags. | Candidate control envelope. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L171-L176", + "text": "The control-plane codec serializes only plugin-owned primitive data: integer\nstage keys, token-count lists, max counts, and graph/warmup/profile boolean\nflags. It encodes JSON,\nsends a size followed by a `uint8` tensor, and reconstructs\n`AFDControlPayload`; it does not serialize a vLLM `DPMetadata` object. Tensor\ndata-path layout remains connector-specific." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L301-L302", + "text": "- `XFER-INV-001`: control-plane wire data uses plugin-owned payloads rather\n than serialized vLLM-internal metadata objects." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L317-L320", + "text": "Changes must validate topology, process-group lifecycle, payload round trips,\nand connector-specific cleanup using the unit paths above. A payload change\nmust update all producers, consumers, codec tests, model handoffs, and both role\nruntimes. A process-group change requires review against the pinned PyTorch and" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L244-L262", + "text": "## Control-plane coordination\n\nWhen `connector.control_plane` is not `None`, the runner sends an\n`AFDControlPayload` through that `AFDControlPlane` before model/data-plane\ntransfer. The payload contains a stage-indexed DP metadata map plus\n`is_warmup`, `is_graph_capturing`, and `is_profile` flags.\n\n- A non-ubatched request sends stage `0` and uses native or fallback DP\n metadata.\n- Native ubatching sends one DP metadata item per stage.\n- A padded full-graph request sends metadata for the padded capture token\n count.\n- An NPU V2 profile forward marks `is_profile` so FFN recreates the matching\n Ascend profile context and balanced dummy-MoE state.\n- The control plane updates its owning connector's local state before sending\n the payload.\n\n`P2pNcclAFDConnector` and `CAMP2pAFDConnector` install\n`P2pNcclAFDControlPlane` and `CAMP2pAFDControlPlane`, respectively." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L315-L327", + "text": "During graph warmup/capture, the Attention runner marks control payloads so the\nFFN daemon can warm or capture the matching shape. A non-ubatched capture sends\nthe control payload explicitly before formal capture; an ubatched capture lets\nthe platform wrapper send the exact per-stage shape. CUDA Graph, ACL Graph,\nstream, and wrapper implementation details are owned by\n[execution platforms](execution_platforms.md).\n\nV2 rejects DBO and ubatching. For full-graph capture, the V2 runner wraps the\nnative capture input-preparation seam and publishes exactly one warmup and one\ncapture payload for every native descriptor before the graph body. Native\nfull-graph replay does not create a `ForwardContext`, so an execute-scoped\nmanager hook publishes the padded control shape immediately before replay.\nAll temporary hooks and pending metadata state are restored in `finally`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L153-L156", + "text": "Attention treats DP metadata transfer as a control-plane side effect. For a\nsingle-stage capture it sends the padded capture shape before entering formal\nCUDA Graph capture. For an ubatched capture, `AFDUBatchWrapper` supplies the\nexact stage slices and sends per-stage metadata before the graph body." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L166-L170", + "text": "FFN uses the Attention payload's warmup/capture flags. It creates a shared\nCUDA graph memory pool, uses `connector.control_plane` to update the owning\nconnector state before `torch.cuda.graph(...)`, captures only\nmodel/data-plane work, and stores the graph by `make_ffn_graph_key()`. A\nmatching future payload replays the graph; a missing key runs eagerly." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L287-L290", + "text": "The FFN runner owns a separate ACL graph cache keyed by stage token counts and\nA/F topology. Warmup runs the eager FFN path. Formal capture updates connector\nstate through `connector.control_plane` before entering `torch.npu.graph(...)`,\nso replay contains only model and data-plane operations. An unknown key falls" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I10", + "note": "kb init skeleton: judge said pass; on main the text is kept", + "rule_id": "AFD-I10", + "source": "owner", + "stage": "skeleton", + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I11.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I11.json new file mode 100644 index 00000000..6d939af9 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I11.json @@ -0,0 +1,67 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L173-L178", + "text": "AFD is configured through vLLM `--additional-config`. There is no separate\n`--afd-config` flag. When AFD is configured and `--worker-cls` is omitted, the\nplugin automatically selects the Attention or FFN worker for the active CUDA\nor standard Ascend NPU platform. Explicit AFD worker paths remain accepted for\ncompatibility with existing commands, but are not required or stable launch\ninterfaces." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L274-L281", + "text": "`role` must be `attention` or `ffn`. `connector` must be `P2pNcclAFDConnector`,\n`CAMP2pAFDConnector`, or `CAMAsyncAFDConnector`. AFD is active when\n`additional_config[\"afd\"]` is present and passes common AFD config validation;\nomit `additional_config[\"afd\"]` to disable AFD. Connector-owned\n`connector_extra_config` is strictly validated by the selected connector parser\nwhen the connector/runtime is constructed. The plugin also accepts selected\ncompatibility aliases such as `afd_role`, `afd_connector`, `afd_host`, and\n`afd_port`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L86-L110", + "text": "The vLLM plugin entry point applies compatibility in this order:\n\n1. perform the non-strict vLLM version check;\n2. import `async_dp_engine`, `async_dp_forward_context`, `config_validation`,\n and `engine_core` in one best-effort block;\n3. register the plugin-owned CUDA benchmark routing strategy as a required\n step;\n4. register the plugin-owned DBO yield operator in a best-effort block;\n5. register model mappings, which is required for registration to complete.\n\nAscend patching is intentionally deferred until the Ascend plugin has finished\nits own platform initialization. AFD config normalization installs the\nplatform-config wrapper through the config facade; NPU worker startup calls the\nidempotent runtime facade, which verifies that wrapper and installs the MLA\ngraph resolver. The FFN worker imports the force-load-balance patch at worker\nconstruction, after Ascend platform initialization and before delegating to\n`NPUWorker.__init__()`. CUDA-only plugin registration does not import\nvLLM-Ascend.\n\nPython module import provides process-level one-time execution in the ordinary\npath. Some patches also preserve originals or set explicit sentinels, but this\nis not consistent across the inventory. There is no transaction or rollback:\nbecause core patch imports share one `try`, a failure can leave earlier patches\ninstalled and later ones skipped. Registration logs these failures at debug\nlevel and continues to model registration." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L121-L121", + "text": "| [`config_validation.py`](../../../afd_plugin/compat/patches/config_validation.py): `EngineArgs.create_engine_config`, `VllmConfig.__post_init__` | For AFD-owned ubatching with a non-DeepEP backend, temporarily presents `deepep_low_latency` during upstream validation and restores the configured backend. After upstream platform normalization, maps an initial `worker_cls=\"auto\"` to the role-specific CUDA or standard Ascend AFD worker. | Imported by `register_afd`; accepts the target version, development versions, or missing version metadata. Saves originals on upstream modules under AFD-specific attributes before installing wrappers. Explicit worker paths and non-AFD configs are not remapped. | [`test_config_validation.py`](../../../tests/unit/compat/patches/test_config_validation.py) covers backend relaxation, four role/platform mappings, explicit and non-AFD preservation, repeated validation, unsupported platforms, and dev versions. | Remove the backend branch when upstream validation distinguishes plugin-owned ubatching; remove worker mapping when vLLM offers plugin-owned role-aware worker selection. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L175-L178", + "text": "- Patch import errors are currently best effort at plugin startup and may be\n visible only in debug logs. Runtime paths must therefore fail explicitly if\n a required adaptation is absent rather than silently serving incorrect\n results." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L74-L76", + "text": "AFD does not add an AFD-specific CLI flag. Configuration is read from vLLM\n`additional_config[\"afd\"]`, and config normalization selects the role-specific\nworker for the active platform when `worker_cls=\"auto\"`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L331-L334", + "text": "Initialization fails early for a missing or role-mismatched AFD config, an\nunsupported connector/feature combination, an implicit or incorrect worker\nclass, invalid V2 topology, invalid native ubatch count, or unsupported graph\nmode." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L204-L205", + "text": "2. apply the non-sequence-parallel all-to-all backend correction when needed,\n including legacy explicit-worker launches;" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L212-L215", + "text": "The all-to-all correction selects `flashinfer_all2allv` when sequence\nparallelism is disabled. Automatic worker selection receives the matching\nupstream default-worker rewrite during config normalization; the worker-side\ncorrection remains as a fallback for legacy explicit-worker launches." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L68-L69", + "text": "FFN is launched as a `vllm serve` process, and AFD config normalization selects\nits role-specific worker when `worker_cls=\"auto\"`. It does not serve requests." + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I11", + "note": "kb init skeleton: judge said pass; on main the text is kept", + "rule_id": "AFD-I11", + "source": "owner", + "stage": "skeleton", + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I12.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I12.json new file mode 100644 index 00000000..e72a612d --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I12.json @@ -0,0 +1,42 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L240-L250", + "text": "uv run python tests/e2e/runner.py \\\n --model /path/to/DeepSeek-V2-Lite \\\n --device-backend gpu \\\n --scenario afd-eager-2a1f \\\n --attention-devices 0,1 \\\n --ffn-devices 2 \\\n --gsm8k-output-path /tmp/afd-e2e-gsm8k \\\n --api-port-base 18000 \\\n --afd-port 6239 \\\n --common-vllm-arg=--trust-remote-code\n```" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/e2e_testing.md:L74-L95", + "text": "- `E2E-INV-001` — A case **MUST** have a stable lower-kebab-case ID and cover\n behavior not already covered by an existing case.\n- `E2E-INV-002` — A PR case **MUST NOT** use more than four unique devices.\n Gate AFD cases **MUST** use 2 Attention ranks and 2 FFN ranks; 2A1F cases\n are local-only.\n- `E2E-INV-003` — Cases sharing devices or ports **MUST** run sequentially,\n remain order-independent, and release owned process groups before the next\n case.\n- `E2E-INV-004` — Gate cases **MUST** fail on missing setup, service failure,\n incomplete results, failed validation, or failed cleanup. They **MUST NOT**\n use `skip`, `xfail`, or success-on-empty behavior.\n- `E2E-INV-005` — The harness **MUST** check service liveness before and after\n evaluation. Accuracy cases **MUST** also check evaluator exit status, sample\n count, `NaN`, and accuracy.\n- `E2E-INV-006` — Child processes **MUST** use owned process groups.\n Cancellation **MUST** send `SIGTERM`, use a bounded grace period, then reap\n every leader. A harness `SIGKILL` escalation **MUST** fail the case.\n- `E2E-INV-007` — CI **MUST NOT** lower the sample count, accuracy threshold,\n or required case set. It **MUST** select cases by pytest node ID.\n- `E2E-INV-008` — A shared case **MUST** pass without skips on every platform\n where CI selects it. New behavior **MUST** have focused unit tests and real\n hardware evidence." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/e2e_testing.md:L116-L141", + "text": "`afd-eager-async-cam` is a separate NPU-only smoke test. It uses four devices\nfor Attention DP1/TP2 and FFN DP2/TP1/EP2. It is not part of the PR gate above.\nThe NPU async CAM cases may require SIGKILL escalation because a pending CAM\nreceive cannot be interrupted cleanly. For these scenarios, the runner\nadds a unique run id and role marker to each launched process environment.\nAfter normal process-group cleanup, it finds every FFN process carrying those\nmarkers through `/proc/*/environ` and sends SIGKILL to each matching PID. This\nalso finds workers that changed process groups or were reparented after their\nlauncher exited, without selecting another concurrent E2E run. NPU workers can\nstay blocked in uninterruptible driver teardown for tens of seconds after\nSIGKILL (measured up to ~45s on A3), so this path waits up to 120s before\nreporting survivors. Marker-scan signal-delivery and survivor failures, plus\nnormal process-reaping failures, remain fatal. This test-scoped exception\nshould be removed when the runtime supports graceful cancellation.\n\n`afd-dsv4-flash-async-cam-dp2tp4-ep8` is a separate local-only DSV4 Flash\nW8A8 case using 16 Ascend NPUs: Attention DP2/TP4 and FFN DP8/TP1/EP8.\nIt uses eager async CAM with two token-split MoE ubatches, MBT=8192, and\n`enable_dsv4_shared_compressor_workspace=false` on both roles. It validates\nten simultaneous chat requests, records their outputs and overlapping\nrequest intervals, and checks service liveness and cleanup. Its cancellable\nasync HTTP client saves per-request responses/errors even on failure or\ninterruption, before the runner tears down services. Model-specific fixed\nsettings live alongside the model entrypoint. It does not run\nGSM8K or claim general accuracy coverage. It uses the same scoped async NPU\nFFN cleanup exception above and is not selected by the four-device PR gate." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/e2e_testing.md:L143-L157", + "text": "The 2A1F cases (`afd-eager-2a1f`, `afd-graph-2a1f`, `afd-graph-dbo-2a1f`) are\nlocal-only scenarios: they use three of the four devices (two Attention ranks,\none FFN rank) and run outside CI.\n\nThe ModelRunnerV2 matrix is CUDA-only in the current E2E harness. Its local\n1A1F cases are `afd-v2-eager-1a1f` and `afd-v2-graph-1a1f`; CI selects the four\nDP2/TP2 cases listed above by exact node ID on `l4_4`. The Ascend\nModelRunnerV2 implementation currently has focused unit evidence but no E2E\ncase, so it is not included in the hardware gate.\n\nThe Qwen3.5/3.6 adapter family has text-only CUDA E2E evidence through\n`Qwen/Qwen3.6-35B-A3B`, using the native Qwen3.5/3.6 model boundary with\n`--language-model-only`. Its default suite uses native DP4/TP1/EP4 for\n`baseline-graph`, and\nsynchronous AFD 2A1F for `afd-eager`, `afd-graph`, and `afd-graph-dbo`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/e2e_testing.md:L164-L186", + "text": "| Setting | PR | Weekly |\n| --- | ---: | ---: |\n| Task | GSM8K | GSM8K |\n| Few-shot examples | 8 | 8 |\n| Generated-token limit | 512 | 512 |\n| Samples | first 7; DBO gates floor at 24 | first 7; the DeepSeek `afd-graph-dbo-2a1f` case floors at 24 |\n| Metric | GSM8K exact match | GSM8K exact match |\n| Minimum accuracy | DeepSeek-V2-Lite smoke (up to 24 samples): 0.25; otherwise 0.27 | DeepSeek-V2-Lite smoke (up to 24 samples): 0.25; otherwise 0.27 |\n| Cases | four legacy cases plus four CUDA ModelRunnerV2 cases | six Qwen3 MoE / Qwen3.6 MoE cases plus DeepSeek-V2-Lite `afd-graph-dbo-2a1f` |\n\nAn accuracy of `0.27` requires at least 2 correct answers out of 7 (7 out\nof 24 in DBO scenarios).\n\n- PR and weekly CI leave `AFD_GSM8K_LIMIT` unset.\n- Set `AFD_GSM8K_LIMIT=all` locally for a full 1319-sample run.\n- Other limits are for local debugging, not CI gates.\n- DBO scenarios (`--enable-dbo`) run GSM8K with 12 concurrent requests and\n floor the sample count at 24: a sequential client never satisfies the\n DP-wide split agreement, so live requests would never run as two ubatches\n and only warmup/capture would exercise the split path. The gate asserts\n that at least one live two-ubatch step was recorded; `AFD_GSM8K_LIMIT=all`\n still bypasses the floor.\n- CI leaves `AFD_GSM8K_THRESHOLD` unset or raises it." + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I12", + "note": "kb init skeleton: judge said unsure; on main the text is kept", + "rule_id": "AFD-I12", + "source": "owner", + "stage": "skeleton", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I13.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I13.json new file mode 100644 index 00000000..e941412f --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I13.json @@ -0,0 +1,52 @@ +{ + "base": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L21-L24", + "text": "The target runtime is **vLLM `v0.26.0`**. The plugin does not modify the vLLM\nsource tree. AFD behavior is installed through the `vllm.general_plugins` entry\npoint, `--additional-config`, automatically selected role workers, plugin-owned\nmodel wrappers, and narrow version-scoped compatibility shims." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L103-L106", + "text": "uv sync --group dev --extra vllm\n```\n\nThe optional extra pins `vllm==0.26.0`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L59-L65", + "text": "## Supported upstream boundary\n\nThe package extra pins vLLM `0.26.0`, and\n[`compat/vllm.py`](../../../afd_plugin/compat/vllm.py) enforces the same target.\nDirect strict calls raise for a missing or different vLLM; plugin registration\ncalls the check with `strict=False`, so it warns and continues. This warning\npolicy does not make another vLLM release supported." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L119-L121", + "text": "| [`async_dp_engine.py`](../../../afd_plugin/compat/patches/async_dp_engine.py): `EngineCoreProc.run_engine_core`, `vllm.v1.engine.utils.launch_core_engines` and its imported client alias, `DPCoordinatorProc.run_coordinator`, `DPCoordinatorProc.process_input_socket`, `DPAsyncMPClient.add_request_async` | Async-DP Attention selects regular `EngineCoreProc`, keeps asynchronous coordinator stats but disables DP wave coordination and `FIRST_REQ`; other configs run the copied upstream branches. | Imported by `register_afd`; general async bindings keep the target/dev guard, while the copied coordinator is exact-`0.26.0` only. Its plugin-owned process target reapplies the loop in spawn children; newer development versions retain their native coordinator. | [`test_async_dp_engine.py`](../../../tests/unit/compat/patches/test_async_dp_engine.py) covers spawn import, exact-version isolation, process selection, non-AFD MoE DP, coordinator mode, wakeup, and reload. | Remove when vLLM exposes role-selectable async-DP engine scheduling, wave policy, and wakeup hooks. |\n| [`async_dp_forward_context.py`](../../../afd_plugin/compat/patches/async_dp_forward_context.py): `vllm.forward_context.set_forward_context` plus already-imported worker aliases | Skips native `DPMetadata` construction/coordination only for AFD async-DP; otherwise uses the copied upstream flow. | Imported by `register_afd`; same target/dev/unknown guard. Rebinds known already-imported aliases so callers do not retain the old function. | [`test_async_dp_forward_context.py`](../../../tests/unit/compat/patches/test_async_dp_forward_context.py) covers async skip and non-async coordination. | Remove when vLLM supports a per-engine-role opt-out from native MoE DP metadata coordination. |\n| [`config_validation.py`](../../../afd_plugin/compat/patches/config_validation.py): `EngineArgs.create_engine_config`, `VllmConfig.__post_init__` | For AFD-owned ubatching with a non-DeepEP backend, temporarily presents `deepep_low_latency` during upstream validation and restores the configured backend. After upstream platform normalization, maps an initial `worker_cls=\"auto\"` to the role-specific CUDA or standard Ascend AFD worker. | Imported by `register_afd`; accepts the target version, development versions, or missing version metadata. Saves originals on upstream modules under AFD-specific attributes before installing wrappers. Explicit worker paths and non-AFD configs are not remapped. | [`test_config_validation.py`](../../../tests/unit/compat/patches/test_config_validation.py) covers backend relaxation, four role/platform mappings, explicit and non-AFD preservation, repeated validation, unsupported platforms, and dev versions. | Remove the backend branch when upstream validation distinguishes plugin-owned ubatching; remove worker mapping when vLLM offers plugin-owned role-aware worker selection. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L167-L169", + "text": "When the pinned upstream version changes, copied bodies must be refreshed from\nthat version before reapplying marked AFD deltas. Passing unit tests against a\ndifferent signature is not sufficient compatibility evidence." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L197-L198", + "text": "- `PATCH-INV-003`: unsupported upstream versions are not treated as supported\n merely because best-effort registration continued." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L204-L207", + "text": "Patch signatures and copied code must match their pinned upstream symbols.\nEvery modification requires focused tests for AFD and non-AFD behavior plus\nthe architectural review required by `AGENTS.md`. Version guard changes also\nrequire package compatibility tests. Ascend patch refreshes require an exact" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I13", + "note": "kb init skeleton: judge said pass; on main the text is kept", + "rule_id": "AFD-I13", + "source": "owner", + "stage": "skeleton", + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I14.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I14.json new file mode 100644 index 00000000..30d0b0c0 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I14.json @@ -0,0 +1,37 @@ +{ + "base": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L122-L125", + "text": "| [`engine_core.py`](../../../afd_plugin/compat/patches/engine_core.py): `EngineCore.__init__`, `_initialize_kv_caches`, `shutdown`; `EngineCoreProc.run_busy_loop`; `DPEngineCoreProc.run_busy_loop` | AFD FFN becomes a connector daemon: construct executor, skip scheduler/KV setup, return an empty KV-shaped result on late paths, start/monitor/stop the FFN worker loop, and use FFN-safe shutdown. Non-FFN branches copy pinned upstream behavior. | Imported by `register_afd`; **no patch-local version guard and no saved-original sentinel**. Direct class assignment means the package pin and review discipline are the compatibility guard. | [`test_engine_core.py`](../../../tests/unit/compat/patches/test_engine_core.py) covers FFN initialization, non-FFN behavior, and daemon start/stop; role runtime tests cover error propagation. | Remove when vLLM offers a headless connector-daemon engine lifecycle or an executor mode that does not require scheduler/KV ownership. |\n| [`npu/ascend_platform.py`](../../../afd_plugin/compat/patches/npu/ascend_platform.py): `NPUPlatform.check_and_update_config` | Snapshots AFD DBO state, runs upstream normalization, and restores configured `enable_dbo`, `ubatch_size`, and `all2all_backend` in `finally`; non-AFD behavior is unchanged. | Installed through the config facade during AFD NPU normalization and verified again by the worker runtime facade; no version guard. Saves the original on the class and uses a class sentinel. The runtime facade caches success only after the wrapper is installed, so an early missing vLLM-Ascend import remains retryable. | [`test_runtime.py`](../../../tests/unit/compat/test_runtime.py) and [`test_npu_runtime.py`](../../../tests/unit/v1/worker/test_npu_runtime.py). | Remove when vLLM-Ascend recognizes plugin-owned DBO workers or no longer clears these fields. |\n| [`npu/mla_graph.py`](../../../afd_plugin/compat/patches/npu/mla_graph.py): `vllm_ascend.attention.mla_v1.get_graph_params` | Resolves an AFD ubatch-owned `GraphParams` registry from the active forward context and otherwise delegates to the saved upstream process-global resolver. | Called through `apply_afd_ascend_patches_if_needed` during NPU worker startup; no version guard. Saves the original on the upstream module and uses a module sentinel. | [`test_runtime.py`](../../../tests/unit/compat/test_runtime.py) covers AFD-context resolution, upstream fallback, and idempotence; [`test_npu_mla_graph.py`](../../../tests/unit/v1/worker/test_npu_mla_graph.py) covers the owning graph lifecycle. | Remove when vLLM-Ascend accepts a forward-context-local MLA graph registry or exposes an equivalent resolver hook. |\n| [`npu/force_load_balance.py`](../../../afd_plugin/compat/patches/npu/force_load_balance.py): `AscendW8A8DynamicFusedMoEMethod.__init__`, `AscendW8A8DynamicFusedMoEMethod.apply` | Captures AFD profiling configuration as method-owned state and replaces routed expert IDs with a deterministic balanced buffer only when the method-owned switch is enabled; normal model-selected routing remains unchanged. This switch changes outputs and is not a correctness feature. | Imported by the NPU FFN worker after vLLM-Ascend platform initialization; **no patch-local version guard or explicit reload sentinel**. Functions copy the current upstream bodies with marked AFD deltas. | [`test_force_load_balance.py`](../../../tests/unit/compat/patches/test_force_load_balance.py) covers buffer bounds, determinism, growth, override, and pass-through. | Upstream a deterministic expert-routing profiling hook in vLLM-Ascend, then delete both copied functions. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L153-L169", + "text": "For every new patch or upstream upgrade, reviewers must:\n\n1. identify the exact upstream file, symbol, version/tag, and signature;\n2. confirm that the patch targets the owning upstream layer rather than hiding\n AFD-owned behavior in a convenient global hook;\n3. copy the pinned upstream function when feasible and mark only AFD deltas\n with `# ### PATCH START` / `# ### PATCH END`;\n4. document any original-function delegation exception immediately above the\n function;\n5. test both the selected AFD branch and the non-AFD/upstream branch, including\n initialization, failure, shutdown, and idempotence where relevant;\n6. assess hot-path, graph/compile, memory, and distributed-lifecycle impact;\n7. record an upstream contribution or concrete removal condition.\n\nWhen the pinned upstream version changes, copied bodies must be refreshed from\nthat version before reapplying marked AFD deltas. Passing unit tests against a\ndifferent signature is not sufficient compatibility evidence." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L179-L181", + "text": "- Copied non-AFD branches are intended to preserve pinned upstream behavior;\n they still require regression tests because global replacement affects every\n process that loads the plugin." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L216-L217", + "text": "- `engine_core` and force-load-balance lack patch-local version/idempotence\n guards;" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I14", + "note": "kb init skeleton: judge said pass; on main the text is kept", + "rule_id": "AFD-I14", + "source": "owner", + "stage": "skeleton", + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I16.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I16.json new file mode 100644 index 00000000..8e53f5de --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I16.json @@ -0,0 +1,22 @@ +{ + "base": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L119-L120", + "text": "| [`async_dp_engine.py`](../../../afd_plugin/compat/patches/async_dp_engine.py): `EngineCoreProc.run_engine_core`, `vllm.v1.engine.utils.launch_core_engines` and its imported client alias, `DPCoordinatorProc.run_coordinator`, `DPCoordinatorProc.process_input_socket`, `DPAsyncMPClient.add_request_async` | Async-DP Attention selects regular `EngineCoreProc`, keeps asynchronous coordinator stats but disables DP wave coordination and `FIRST_REQ`; other configs run the copied upstream branches. | Imported by `register_afd`; general async bindings keep the target/dev guard, while the copied coordinator is exact-`0.26.0` only. Its plugin-owned process target reapplies the loop in spawn children; newer development versions retain their native coordinator. | [`test_async_dp_engine.py`](../../../tests/unit/compat/patches/test_async_dp_engine.py) covers spawn import, exact-version isolation, process selection, non-AFD MoE DP, coordinator mode, wakeup, and reload. | Remove when vLLM exposes role-selectable async-DP engine scheduling, wave policy, and wakeup hooks. |\n| [`async_dp_forward_context.py`](../../../afd_plugin/compat/patches/async_dp_forward_context.py): `vllm.forward_context.set_forward_context` plus already-imported worker aliases | Skips native `DPMetadata` construction/coordination only for AFD async-DP; otherwise uses the copied upstream flow. | Imported by `register_afd`; same target/dev/unknown guard. Rebinds known already-imported aliases so callers do not retain the old function. | [`test_async_dp_forward_context.py`](../../../tests/unit/compat/patches/test_async_dp_forward_context.py) covers async skip and non-async coordination. | Remove when vLLM supports a per-engine-role opt-out from native MoE DP metadata coordination. |" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I16", + "note": "kb init skeleton: judge said pass; on main the text is kept", + "rule_id": "AFD-I16", + "source": "owner", + "stage": "skeleton", + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I17.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I17.json new file mode 100644 index 00000000..f31c54a8 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I17.json @@ -0,0 +1,52 @@ +{ + "base": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L86-L94", + "text": "The vLLM plugin entry point applies compatibility in this order:\n\n1. perform the non-strict vLLM version check;\n2. import `async_dp_engine`, `async_dp_forward_context`, `config_validation`,\n and `engine_core` in one best-effort block;\n3. register the plugin-owned CUDA benchmark routing strategy as a required\n step;\n4. register the plugin-owned DBO yield operator in a best-effort block;\n5. register model mappings, which is required for registration to complete." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L105-L110", + "text": "Python module import provides process-level one-time execution in the ordinary\npath. Some patches also preserve originals or set explicit sentinels, but this\nis not consistent across the inventory. There is no transaction or rollback:\nbecause core patch imports share one `try`, a failure can leave earlier patches\ninstalled and later ones skipped. Registration logs these failures at debug\nlevel and continues to model registration." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L175-L178", + "text": "- Patch import errors are currently best effort at plugin startup and may be\n visible only in debug logs. Runtime paths must therefore fail explicitly if\n a required adaptation is absent rather than silently serving incorrect\n results." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L199-L200", + "text": "- `PATCH-INV-004`: patch initialization is idempotent or explicitly guarded,\n and partial application is observable before the affected runtime executes." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L230-L232", + "text": "Attention and FFN may be started in either order. Send requests only to the\nAttention API server. FFN workers are connector-driven; scheduler-driven FFN\n`execute_model()` calls fail fast." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L329-L336", + "text": "## Failure and cleanup behavior\n\nInitialization fails early for a missing or role-mismatched AFD config, an\nunsupported connector/feature combination, an implicit or incorrect worker\nclass, invalid V2 topology, invalid native ubatch count, or unsupported graph\nmode.\nMissing DP metadata for DP greater than 1 and missing pending metadata for a\nfallback are runtime errors rather than silently guessed shapes." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L115-L123", + "text": "FFN does not own request KV blocks. Both workers return an empty KV-cache spec,\nand both runners no-op KV-cache initialization. `compile_or_warm_up_model()`\nreturns `0.0`; warmup and graph capture, when supported, are driven later by\nconnector metadata. Because the FFN EngineCore skips that upstream warmup path,\nthe NPU worker performs vLLM-Ascend's CPU-binding step once immediately before\nstarting its connector daemon when `enable_cpu_binding` is enabled. The\nEngineCore compatibility patch keeps FFN daemon mode out of upstream\nscheduler/KV-cache startup assumptions and selects the daemon busy loop. See\n[compatibility and patches](compatibility_and_patches.md)." + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I17", + "note": "kb init skeleton: judge said unsure; on main the text is kept", + "rule_id": "AFD-I17", + "source": "owner", + "stage": "skeleton", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I18.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I18.json new file mode 100644 index 00000000..3e32ff47 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I18.json @@ -0,0 +1,62 @@ +{ + "base": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L94-L95", + "text": "`vllm` is an optional runtime extra so CPU-only or macOS development\nenvironments can still run import/config tests without a CUDA wheel." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L285-L293", + "text": "Run the default CPU-safe checks:\n\n```bash\nuv run pytest\nuv run ruff check .\n```\n\nThe NPU-dependent unit suite must run in an Ascend development environment;\nsee [Running NPU unit tests](docs/npu/TESTING.md)." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L91-L94", + "text": "Runtime modules import their real device dependencies. CPU safety applies to\nthe top-level package, common configuration/validation, version checks, and\nthe graph policy helper; importing a CUDA or Ascend runtime module requires\nthe corresponding runtime stack." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L141-L144", + "text": "### CUDA Graph policy\n\n`validate_cuda_graph_mode()` resolves the shared AFD policy without importing\ntorch or vLLM at module import time. Current behavior is:" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L303-L308", + "text": "The build performs the CANN vendor build under `csrc/npu`, builds the PyTorch\nCMake extension, and packages `_cann_ops_custom`. Loading remains lazy:\n`ensure_cam_p2p_ops_available()` updates the vendor/library environment, imports\n`afd_plugin._C_ascend`, and verifies `torch.ops.afd_ascend.a2e/e2a` only when\nthe connector initializes. The package can therefore be imported without the\nNPU extension, but the CAMP2P data path cannot run without it." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L356-L360", + "text": "## Failure and cleanup boundaries\n\nUnsupported graph, ubatch, async, gate, or quantization combinations fail in\nconfiguration/worker/runner initialization. Missing native operators fail at\nconnector initialization rather than package import. Device graph caches and" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L67-L68", + "text": "`AFDConnectorFactory` stores lazy loaders, so importing the factory does not\nimport CUDA or Ascend implementations. Built-in names are registered at module" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L96-L103", + "text": "Ascend patching is intentionally deferred until the Ascend plugin has finished\nits own platform initialization. AFD config normalization installs the\nplatform-config wrapper through the config facade; NPU worker startup calls the\nidempotent runtime facade, which verifies that wrapper and installs the MLA\ngraph resolver. The FFN worker imports the force-load-balance patch at worker\nconstruction, after Ascend platform initialization and before delegating to\n`NPUWorker.__init__()`. CUDA-only plugin registration does not import\nvLLM-Ascend." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L123-L123", + "text": "| [`npu/ascend_platform.py`](../../../afd_plugin/compat/patches/npu/ascend_platform.py): `NPUPlatform.check_and_update_config` | Snapshots AFD DBO state, runs upstream normalization, and restores configured `enable_dbo`, `ubatch_size`, and `all2all_backend` in `finally`; non-AFD behavior is unchanged. | Installed through the config facade during AFD NPU normalization and verified again by the worker runtime facade; no version guard. Saves the original on the class and uses a class sentinel. The runtime facade caches success only after the wrapper is installed, so an early missing vLLM-Ascend import remains retryable. | [`test_runtime.py`](../../../tests/unit/compat/test_runtime.py) and [`test_npu_runtime.py`](../../../tests/unit/v1/worker/test_npu_runtime.py). | Remove when vLLM-Ascend recognizes plugin-owned DBO workers or no longer clears these fields. |" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I18", + "note": "kb init skeleton: judge said pass; on main the text is kept", + "rule_id": "AFD-I18", + "source": "owner", + "stage": "skeleton", + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I19.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I19.json new file mode 100644 index 00000000..3fc85add --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I19.json @@ -0,0 +1,62 @@ +{ + "base": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L59-L59", + "text": "| `CAMAsyncAFDConnector` | Ascend NPU | Prefill / decode | Async | Not supported | Experimental v0.26 DP+TP/SP path with AFD-managed two-stage MoE ubatching; native DBO and PCP are unsupported. Post-fix DeepSeek-V3.2 DP2TP8+EP16 token split reached `0.9522` strict match on the complete GSM8K evaluation. The [legacy PCP8 recipe](recipe/npu/CAMAsyncAFDConnector/deepseek_v3_2/README.md) requires `release/v0.19.1rc1`. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L81-L82", + "text": "- PCP-based NPU model-runner-v1 deployments from v0.19.1rc1 are not supported\n on v0.26." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L114-L127", + "text": "| Component | Version |\n| --- | --- |\n| Python | `3.10` or `3.11` |\n| vLLM | `0.26.0` |\n| vLLM-Ascend | commit [`80d8c194f`](https://github.com/vllm-project/vllm-ascend/commit/80d8c194f7584b17fe08065ea99a130916f6b0e7) |\n| CANN / torch / torch-npu | Use the mutually compatible versions required by that vLLM-Ascend source snapshot. |\n\n#### Environment\n\nThe v0.26 integration was refreshed against vLLM-Ascend commit `80d8c194f`;\nthe repository does not currently claim a released v0.26 container tag. Use the\n[installation guide at that source snapshot](https://github.com/vllm-project/vllm-ascend/blob/80d8c194f7584b17fe08065ea99a130916f6b0e7/docs/source/installation.md)\nto prepare a matching A3/openEuler environment, then install AFD from the\nrepository root. Do not reuse the former v0.19.1rc1 image as a v0.26 runtime." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L292-L293", + "text": "The NPU-dependent unit suite must run in an Ascend development environment;\nsee [Running NPU unit tests](docs/npu/TESTING.md)." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L67-L71", + "text": "The NPU v0.26 refresh is based on vLLM-Ascend commit\n[`80d8c194f`](https://github.com/vllm-project/vllm-ascend/commit/80d8c194f7584b17fe08065ea99a130916f6b0e7).\nThe repository does not declare a vLLM-Ascend package dependency in\n`pyproject.toml`, so this source commit and the recorded NPU validation are the\ncompatibility evidence rather than a released package or container tag." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L207-L209", + "text": "require package compatibility tests. Ascend patch refreshes require an exact\nvLLM-Ascend source reference and the NPU unit/E2E evidence for the affected\npath." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L344-L344", + "text": "| Ascend + `CAMAsyncAFDConnector` | Eager only | Native DBO rejected; optional AFD-managed MoE ubatching uses exactly two request or token-balanced stages | Experimental v0.26 port; `async=true`; documented path uses common `compute_gate_on_attention=true`; token mode requires Attention TP > 1; model runner v1 PCP is unsupported; prefill and decode context parallelism are unsupported; `connector_extra_config.dynamicQuant` is 0 or 1; plugin-owned async CAM ops required | Focused unit coverage; pre-fix DP3TP2/EP2 six-case E2E matrix; post-fix full 61-layer DP2TP8+EP16 token-split run reached `0.9522` strict match on the complete GSM8K evaluation |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L352-L354", + "text": "The repository does not record a canonical CUDA container or a released\nvLLM-Ascend v0.26 container. The NPU implementation records source commit\n`80d8c194f`; environment evidence is not an authoritative package tag." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L295-L295", + "text": "- does not support prefill or decode context parallel metadata." + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I19", + "note": "kb init skeleton: judge said pass; on main the text is kept", + "rule_id": "AFD-I19", + "source": "owner", + "stage": "skeleton", + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I2.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I2.json new file mode 100644 index 00000000..c7ed6931 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I2.json @@ -0,0 +1,57 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L126-L129", + "text": "`AFDConnectorBase` defines the lifecycle and data-plane shape below. Its\n`control_plane: AFDControlPlane | None` attribute is the explicit runtime\nselector: synchronous connectors install a control-plane object during\nconstruction, while CAM async leaves it as `None`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L275-L279", + "text": "The worker owns the connector object and calls `close()`. The connector owns\nthe groups/communicators it creates and the control-plane object it exposes;\nthe control plane does not have an independent lifetime. Model code may invoke\nconnector data-path methods through forward metadata but does not own those\nresources." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L289-L291", + "text": "- Callers must branch on `connector.control_plane is None` before using the\n control-plane interface. GPU runners require a non-`None` control plane;\n CAM async instead exposes connector-driven work-item methods." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L307-L309", + "text": "- `CAP-INV-001`: runtimes choose control-driven or connector-driven FFN steps\n from the explicit optional `connector.control_plane` interface, not a\n concrete class-name check." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L150-L164", + "text": "## Daemon step selection\n\nThe worker selects one of two FFN step paths from the optional\n`connector.control_plane` interface:\n\n| Selection state | Connectors | Worker behavior |\n| --- | --- | --- |\n| `control_plane is not None` | `P2pNcclAFDConnector`, `CAMP2pAFDConnector` | Call `control_plane.recv_dp_metadata_list()`, then profile, warm, capture, replay, or execute its stage map. |\n| `control_plane is None` | `CAMAsyncAFDConnector` (NPU only) | Block directly on a connector work item; no separate DP-metadata control plane. |\n\nThe connector-driven path exists only on Ascend. GPU FFN supports\ncontrol-plane-driven connectors exclusively: `GPUFFNModelRunner` asserts\n`connector.control_plane is not None` at construction, and the GPU daemon loop\nraises `NotImplementedError` if a connector without a control plane is ever\ninstalled." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L219-L220", + "text": "CAM async is eager-only and does not use the FFN graph-control path; the graph\ncache is keyed by DP metadata, which does not exist without a control plane." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L246-L265", + "text": "When `connector.control_plane` is not `None`, the runner sends an\n`AFDControlPayload` through that `AFDControlPlane` before model/data-plane\ntransfer. The payload contains a stage-indexed DP metadata map plus\n`is_warmup`, `is_graph_capturing`, and `is_profile` flags.\n\n- A non-ubatched request sends stage `0` and uses native or fallback DP\n metadata.\n- Native ubatching sends one DP metadata item per stage.\n- A padded full-graph request sends metadata for the padded capture token\n count.\n- An NPU V2 profile forward marks `is_profile` so FFN recreates the matching\n Ascend profile context and balanced dummy-MoE state.\n- The control plane updates its owning connector's local state before sending\n the payload.\n\n`P2pNcclAFDConnector` and `CAMP2pAFDConnector` install\n`P2pNcclAFDControlPlane` and `CAMP2pAFDControlPlane`, respectively.\n`CAMAsyncAFDConnector.control_plane` remains `None`, so the Attention runner\nskips DP metadata coordination; CAM dispatch payloads carry the routing and\ntoken metadata required to drive FFN work." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L287-L292", + "text": "The FFN runner owns a separate ACL graph cache keyed by stage token counts and\nA/F topology. Warmup runs the eager FFN path. Formal capture updates connector\nstate through `connector.control_plane` before entering `torch.npu.graph(...)`,\nso replay contains only model and data-plane operations. An unknown key falls\nback to eager execution. CAM async does not enter this path because validation\nrequires eager execution and `connector.control_plane` is `None`." + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I2", + "note": "kb init skeleton: judge said pass; on main the text is kept", + "rule_id": "AFD-I2", + "source": "owner", + "stage": "skeleton", + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I23.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I23.json new file mode 100644 index 00000000..9f0a822a --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I23.json @@ -0,0 +1,42 @@ +{ + "base": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L173-L178", + "text": "AFD is configured through vLLM `--additional-config`. There is no separate\n`--afd-config` flag. When AFD is configured and `--worker-cls` is omitted, the\nplugin automatically selects the Attention or FFN worker for the active CUDA\nor standard Ascend NPU platform. Explicit AFD worker paths remain accepted for\ncompatibility with existing commands, but are not required or stable launch\ninterfaces." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L121-L121", + "text": "| [`config_validation.py`](../../../afd_plugin/compat/patches/config_validation.py): `EngineArgs.create_engine_config`, `VllmConfig.__post_init__` | For AFD-owned ubatching with a non-DeepEP backend, temporarily presents `deepep_low_latency` during upstream validation and restores the configured backend. After upstream platform normalization, maps an initial `worker_cls=\"auto\"` to the role-specific CUDA or standard Ascend AFD worker. | Imported by `register_afd`; accepts the target version, development versions, or missing version metadata. Saves originals on upstream modules under AFD-specific attributes before installing wrappers. Explicit worker paths and non-AFD configs are not remapped. | [`test_config_validation.py`](../../../tests/unit/compat/patches/test_config_validation.py) covers backend relaxation, four role/platform mappings, explicit and non-AFD preservation, repeated validation, unsupported platforms, and dev versions. | Remove the backend branch when upstream validation distinguishes plugin-owned ubatching; remove worker mapping when vLLM offers plugin-owned role-aware worker selection. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L134-L134", + "text": "| [`compat/npu/runtime_config.py`](../../../afd_plugin/compat/npu/runtime_config.py) | Mirrors vLLM-Ascend's non-SP all-to-all backend rewrite for custom AFD workers and reports the active NPU ubatch count. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L202-L215", + "text": "\n1. validate Ascend-specific feature combinations;\n2. apply the non-sequence-parallel all-to-all backend correction when needed,\n including legacy explicit-worker launches;\n3. validate the synchronous CAMP2P ModelRunnerV2 subset when V2 is selected;\n4. call `NPUWorker._init_device()`;\n5. initialize the vLLM workspace manager for one or two ubatches;\n6. construct the matching V1 or V2 Attention runner, or the connector-driven\n FFN runner.\n\nThe all-to-all correction selects `flashinfer_all2allv` when sequence\nparallelism is disabled. Automatic worker selection receives the matching\nupstream default-worker rewrite during config normalization; the worker-side\ncorrection remains as a fallback for legacy explicit-worker launches." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L72-L76", + "text": "## Runtime selection\n\nAFD does not add an AFD-specific CLI flag. Configuration is read from vLLM\n`additional_config[\"afd\"]`, and config normalization selects the role-specific\nworker for the active platform when `worker_cls=\"auto\"`." + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I23", + "note": "kb init skeleton: judge said unsure; on main the text is kept", + "rule_id": "AFD-I23", + "source": "owner", + "stage": "skeleton", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I3.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I3.json new file mode 100644 index 00000000..f8d2147a --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I3.json @@ -0,0 +1,57 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L105-L123", + "text": "The common Attention initialization sequence is:\n\n```text\nvLLM worker construction\n -> validate AFD config, role, connector, and selected worker class\n -> select and validate the V1 or V2 Attention runner\n -> initialize the matching upstream device worker\n -> construct the AFD Attention model runner\n -> derive the role-local AFD rank from DP/TP ranks when needed\n -> create the configured connector (no communication resources yet)\n -> load the role-aware model\n -> initialize the connector (collective rendezvous with FFN ranks)\n```\n\nThe connector rendezvous is deferred to the end of Attention `load_model()`,\nafter any AFD/Ascend ubatch wrapper is installed, so Attention and FFN weight\nloading overlap across roles. FFN initializes its connector later from\n`initialize_from_config()`; the cross-role rendezvous completes before\nAttention memory profiling or the first model forward." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L338-L342", + "text": "Connector initialization failures remain visible from the end of model\nloading, before memory profiling. Shutdown stops the platform profiler and\ncloses connector-owned resources; closing an uninitialized connector is safe\nand leaves it closeable after partial startup failures. The Ascend runner also\ndelegates to upstream shutdown." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L131-L136", + "text": "| Surface | Caller and current responsibility |\n| --- | --- |\n| `parse_extra_config(raw)` | Connector class parses and validates its closed configuration schema without creating communication resources. |\n| `init_afd_connector()` | Owning worker/runner creates backend groups, communicators, operator registrations, and topology-derived state after the device runtime is ready. |\n| `is_initialized` | Reports whether backend communication resources are usable. |\n| `close()` | Owning runtime releases connector-owned resources during shutdown; cleanup is expected to be safe after partial initialization. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L243-L254", + "text": "Construction is device-light; initialization owns backend discovery and\ncommunication setup. Both role runtimes construct their connectors during\ndevice initialization and defer the collective `init_afd_connector()` until\nafter their model weights are loaded (Attention at the end of\n`load_model()`, FFN from `initialize_from_config()`), so Attention and FFN\nweight loading overlap across roles before the cross-role rendezvous.\nSynchronous connectors receive/apply control metadata before tensor transfer\nso they can derive shapes and graph buffers. CAM async does not use that\ncontrol plane: dispatch payloads determine layer and token shape, and the FFN\nloop blocks on connector work. Closing must happen after work stops and must\nclear connector registries, pending queues, communicators, and process groups\nowned by that connector." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L283-L295", + "text": "- Invalid connector names, subclasses, role ranks, or topology fail before\n communication starts.\n- Missing CUDA/Ascend/CAM operators fail at connector initialization with a\n backend-specific runtime error.\n- Calls that require initialization, transfer state, routing metadata, or a\n matching tensor shape fail at the concrete connector boundary.\n- Callers must branch on `connector.control_plane is None` before using the\n control-plane interface. GPU runners require a non-`None` control plane;\n CAM async instead exposes connector-driven work-item methods.\n- FFN daemon loop failures are propagated by the owning runtime; connectors do\n not swallow compute or communication errors.\n- Partial initialization must remain closeable. Connector changes must test\n cleanup and retry/reinitialization behavior appropriate to their backend." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L100-L113", + "text": "The common FFN initialization sequence is:\n\n```text\nvLLM worker construction\n -> validate AFD config, role, connector, and selected worker class\n -> validate the paired V1 or V2 deployment contract\n -> initialize the matching upstream device worker\n -> construct the AFD FFN model runner\n -> derive the role-local AFD rank from DP/TP ranks when needed\n -> load the role-aware FFN model\n -> initialize an empty KV-cache surface\n -> initialize the connector\n -> start the background FFN loop\n```" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L183-L187", + "text": "```text\nFFN initialize_from_config(...)\n -> initialize empty KV-cache surface\n -> initialize connector\n -> start daemon thread" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L358-L363", + "text": "Unsupported graph, ubatch, async, gate, or quantization combinations fail in\nconfiguration/worker/runner initialization. Missing native operators fail at\nconnector initialization rather than package import. Device graph caches and\nprofilers are runner-owned; process groups and communication handles are\nconnector-owned; workspace/device teardown remains upstream-owned after the\nAFD role releases its resources." + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I3", + "note": "kb init skeleton: judge said pass; on main the text is kept", + "rule_id": "AFD-I3", + "source": "owner", + "stage": "skeleton", + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I4.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I4.json new file mode 100644 index 00000000..f40d6c0b --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I4.json @@ -0,0 +1,52 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L94-L95", + "text": "`vllm` is an optional runtime extra so CPU-only or macOS development\nenvironments can still run import/config tests without a CUDA wheel." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L285-L293", + "text": "Run the default CPU-safe checks:\n\n```bash\nuv run pytest\nuv run ruff check .\n```\n\nThe NPU-dependent unit suite must run in an Ascend development environment;\nsee [Running NPU unit tests](docs/npu/TESTING.md)." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L91-L94", + "text": "Runtime modules import their real device dependencies. CPU safety applies to\nthe top-level package, common configuration/validation, version checks, and\nthe graph policy helper; importing a CUDA or Ascend runtime module requires\nthe corresponding runtime stack." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L143-L144", + "text": "`validate_cuda_graph_mode()` resolves the shared AFD policy without importing\ntorch or vLLM at module import time. Current behavior is:" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L304-L308", + "text": "CMake extension, and packages `_cann_ops_custom`. Loading remains lazy:\n`ensure_cam_p2p_ops_available()` updates the vendor/library environment, imports\n`afd_plugin._C_ascend`, and verifies `torch.ops.afd_ascend.a2e/e2a` only when\nthe connector initializes. The package can therefore be imported without the\nNPU extension, but the CAMP2P data path cannot run without it." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L67-L68", + "text": "`AFDConnectorFactory` stores lazy loaders, so importing the factory does not\nimport CUDA or Ascend implementations. Built-in names are registered at module" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L96-L103", + "text": "Ascend patching is intentionally deferred until the Ascend plugin has finished\nits own platform initialization. AFD config normalization installs the\nplatform-config wrapper through the config facade; NPU worker startup calls the\nidempotent runtime facade, which verifies that wrapper and installs the MLA\ngraph resolver. The FFN worker imports the force-load-balance patch at worker\nconstruction, after Ascend platform initialization and before delegating to\n`NPUWorker.__init__()`. CUDA-only plugin registration does not import\nvLLM-Ascend." + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I4", + "note": "kb init skeleton: judge said pass; on main the text is kept", + "rule_id": "AFD-I4", + "source": "owner", + "stage": "skeleton", + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I5.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I5.json new file mode 100644 index 00000000..7d4f75a2 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I5.json @@ -0,0 +1,67 @@ +{ + "base": { + "repos/afd-plugin/components/compatibility/rules.md": "---\ntitle: \"AFD compatibility 规则\"\ncreated: 2026-08-06\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin, components, compatibility, review]\nsources:\n - \"afd-plugin@a432692:AGENTS.md\"\n - \"afd-plugin@a432692:afd_plugin/compat/**\"\n - \"afd-plugin@a432692:docs/design/module/compatibility_and_patches.md\"\nconfidence: high\n---\n\n# AFD compatibility 规则\n\n这些规则从 [仓库规则](../../rules.md) 迁入最近 owner,保留原有稳定 ID。\n\n## AFD-3a — patch 必须 upstream-first 且可移除\n\n触发:新增 patch、修改已有 patch 或升级 vLLM/vLLM-Ascend。\n\n- 必须记录精确 upstream file/symbol/version/signature,优先复制对应版本函数,并保持签名和返回类型。\n- AFD 差异必须用 `# ### PATCH START: ...` / `# ### PATCH END: ...` 标出;函数上方必须说明 patch reason、行为变化和 upstream/移除条件。\n- 禁止在便利的全局 hook 中放置本应属于 AFD model、worker 或 connector 的功能。\n- 验收:AFD 和 non-AFD 分支、初始化/失败/shutdown 及需要时的 reload/idempotence 都有 focused tests,并写明移除路径。\n\n## AFD-3b — 上游 API drift 必须直接暴露\n\n触发:访问 vLLM/vLLM-Ascend 结构、添加 fallback 或选择 original-function delegation。\n\n- 必须直接访问预期上游函数和字段,让静态检查和原始错误暴露缺失/改名。\n- 禁止用宽泛 `getattr`、`hasattr`、`Any`、`object` 或 `_original_*` delegation 掩盖 drift。只有 `AGENTS.md` 明确允许的超大/不适合内联函数例外才可 delegation,且必须就地解释。\n- 验收:当前 pinned signature 对齐,故意缺失字段不会被 fallback 吞掉,保存的 original 不会在 reload 时被 wrapper 覆盖。\n\npatch 生命周期见 [architecture](architecture.md),版本支持声明见 [仓库规则](../../rules.md)。\n\n## AFD-I6 — 临时替换上游符号的代码必须在 `finally` 里还原,并登记到兼容文档\n\n- 这类临时替换包括:包装 `create_forward_context`、在 `Worker.init_device()` 期间替换 runner 符号、包装 V2 capture 的输入准备、安装 full-graph replay hook、给 DBO 临时关闭 metadata cache。它们都必须在 `finally` 里恢复符号和 runner 状态,异常路径也要恢复。\n- 需要有测试覆盖恢复逻辑和错误路径。参考 `test_attention_model_runner.py` 对 DBO 规避代码的覆盖。\n- 新增的这类替换要写进 `docs/design/module/compatibility_and_patches.md` 的 scoped adaptation 列表,否则升级审计会漏掉它。\n- 需要在整个进程生效的替换,应放到 `afd_plugin/compat/patches/`,并走 patch 审查流程。反过来,AFD 自己的功能不要借全局 hook 藏进 patch 模块。\n- 保存了原函数的 patch,在模块重载时不能用自己的 wrapper 覆盖已保存的原函数。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L21-L24", + "text": "The target runtime is **vLLM `v0.26.0`**. The plugin does not modify the vLLM\nsource tree. AFD behavior is installed through the `vllm.general_plugins` entry\npoint, `--additional-config`, automatically selected role workers, plugin-owned\nmodel wrappers, and narrow version-scoped compatibility shims." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L67-L67", + "text": "- vLLM versions other than `0.26.0` are not claimed as supported." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L106-L106", + "text": "The optional extra pins `vllm==0.26.0`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L114-L119", + "text": "| Component | Version |\n| --- | --- |\n| Python | `3.10` or `3.11` |\n| vLLM | `0.26.0` |\n| vLLM-Ascend | commit [`80d8c194f`](https://github.com/vllm-project/vllm-ascend/commit/80d8c194f7584b17fe08065ea99a130916f6b0e7) |\n| CANN / torch / torch-npu | Use the mutually compatible versions required by that vLLM-Ascend source snapshot. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L61-L71", + "text": "The package extra pins vLLM `0.26.0`, and\n[`compat/vllm.py`](../../../afd_plugin/compat/vllm.py) enforces the same target.\nDirect strict calls raise for a missing or different vLLM; plugin registration\ncalls the check with `strict=False`, so it warns and continues. This warning\npolicy does not make another vLLM release supported.\n\nThe NPU v0.26 refresh is based on vLLM-Ascend commit\n[`80d8c194f`](https://github.com/vllm-project/vllm-ascend/commit/80d8c194f7584b17fe08065ea99a130916f6b0e7).\nThe repository does not declare a vLLM-Ascend package dependency in\n`pyproject.toml`, so this source commit and the recorded NPU validation are the\ncompatibility evidence rather than a released package or container tag." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L119-L125", + "text": "| [`async_dp_engine.py`](../../../afd_plugin/compat/patches/async_dp_engine.py): `EngineCoreProc.run_engine_core`, `vllm.v1.engine.utils.launch_core_engines` and its imported client alias, `DPCoordinatorProc.run_coordinator`, `DPCoordinatorProc.process_input_socket`, `DPAsyncMPClient.add_request_async` | Async-DP Attention selects regular `EngineCoreProc`, keeps asynchronous coordinator stats but disables DP wave coordination and `FIRST_REQ`; other configs run the copied upstream branches. | Imported by `register_afd`; general async bindings keep the target/dev guard, while the copied coordinator is exact-`0.26.0` only. Its plugin-owned process target reapplies the loop in spawn children; newer development versions retain their native coordinator. | [`test_async_dp_engine.py`](../../../tests/unit/compat/patches/test_async_dp_engine.py) covers spawn import, exact-version isolation, process selection, non-AFD MoE DP, coordinator mode, wakeup, and reload. | Remove when vLLM exposes role-selectable async-DP engine scheduling, wave policy, and wakeup hooks. |\n| [`async_dp_forward_context.py`](../../../afd_plugin/compat/patches/async_dp_forward_context.py): `vllm.forward_context.set_forward_context` plus already-imported worker aliases | Skips native `DPMetadata` construction/coordination only for AFD async-DP; otherwise uses the copied upstream flow. | Imported by `register_afd`; same target/dev/unknown guard. Rebinds known already-imported aliases so callers do not retain the old function. | [`test_async_dp_forward_context.py`](../../../tests/unit/compat/patches/test_async_dp_forward_context.py) covers async skip and non-async coordination. | Remove when vLLM supports a per-engine-role opt-out from native MoE DP metadata coordination. |\n| [`config_validation.py`](../../../afd_plugin/compat/patches/config_validation.py): `EngineArgs.create_engine_config`, `VllmConfig.__post_init__` | For AFD-owned ubatching with a non-DeepEP backend, temporarily presents `deepep_low_latency` during upstream validation and restores the configured backend. After upstream platform normalization, maps an initial `worker_cls=\"auto\"` to the role-specific CUDA or standard Ascend AFD worker. | Imported by `register_afd`; accepts the target version, development versions, or missing version metadata. Saves originals on upstream modules under AFD-specific attributes before installing wrappers. Explicit worker paths and non-AFD configs are not remapped. | [`test_config_validation.py`](../../../tests/unit/compat/patches/test_config_validation.py) covers backend relaxation, four role/platform mappings, explicit and non-AFD preservation, repeated validation, unsupported platforms, and dev versions. | Remove the backend branch when upstream validation distinguishes plugin-owned ubatching; remove worker mapping when vLLM offers plugin-owned role-aware worker selection. |\n| [`engine_core.py`](../../../afd_plugin/compat/patches/engine_core.py): `EngineCore.__init__`, `_initialize_kv_caches`, `shutdown`; `EngineCoreProc.run_busy_loop`; `DPEngineCoreProc.run_busy_loop` | AFD FFN becomes a connector daemon: construct executor, skip scheduler/KV setup, return an empty KV-shaped result on late paths, start/monitor/stop the FFN worker loop, and use FFN-safe shutdown. Non-FFN branches copy pinned upstream behavior. | Imported by `register_afd`; **no patch-local version guard and no saved-original sentinel**. Direct class assignment means the package pin and review discipline are the compatibility guard. | [`test_engine_core.py`](../../../tests/unit/compat/patches/test_engine_core.py) covers FFN initialization, non-FFN behavior, and daemon start/stop; role runtime tests cover error propagation. | Remove when vLLM offers a headless connector-daemon engine lifecycle or an executor mode that does not require scheduler/KV ownership. |\n| [`npu/ascend_platform.py`](../../../afd_plugin/compat/patches/npu/ascend_platform.py): `NPUPlatform.check_and_update_config` | Snapshots AFD DBO state, runs upstream normalization, and restores configured `enable_dbo`, `ubatch_size`, and `all2all_backend` in `finally`; non-AFD behavior is unchanged. | Installed through the config facade during AFD NPU normalization and verified again by the worker runtime facade; no version guard. Saves the original on the class and uses a class sentinel. The runtime facade caches success only after the wrapper is installed, so an early missing vLLM-Ascend import remains retryable. | [`test_runtime.py`](../../../tests/unit/compat/test_runtime.py) and [`test_npu_runtime.py`](../../../tests/unit/v1/worker/test_npu_runtime.py). | Remove when vLLM-Ascend recognizes plugin-owned DBO workers or no longer clears these fields. |\n| [`npu/mla_graph.py`](../../../afd_plugin/compat/patches/npu/mla_graph.py): `vllm_ascend.attention.mla_v1.get_graph_params` | Resolves an AFD ubatch-owned `GraphParams` registry from the active forward context and otherwise delegates to the saved upstream process-global resolver. | Called through `apply_afd_ascend_patches_if_needed` during NPU worker startup; no version guard. Saves the original on the upstream module and uses a module sentinel. | [`test_runtime.py`](../../../tests/unit/compat/test_runtime.py) covers AFD-context resolution, upstream fallback, and idempotence; [`test_npu_mla_graph.py`](../../../tests/unit/v1/worker/test_npu_mla_graph.py) covers the owning graph lifecycle. | Remove when vLLM-Ascend accepts a forward-context-local MLA graph registry or exposes an equivalent resolver hook. |\n| [`npu/force_load_balance.py`](../../../afd_plugin/compat/patches/npu/force_load_balance.py): `AscendW8A8DynamicFusedMoEMethod.__init__`, `AscendW8A8DynamicFusedMoEMethod.apply` | Captures AFD profiling configuration as method-owned state and replaces routed expert IDs with a deterministic balanced buffer only when the method-owned switch is enabled; normal model-selected routing remains unchanged. This switch changes outputs and is not a correctness feature. | Imported by the NPU FFN worker after vLLM-Ascend platform initialization; **no patch-local version guard or explicit reload sentinel**. Functions copy the current upstream bodies with marked AFD deltas. | [`test_force_load_balance.py`](../../../tests/unit/compat/patches/test_force_load_balance.py) covers buffer bounds, determinism, growth, override, and pass-through. | Upstream a deterministic expert-routing profiling hook in vLLM-Ascend, then delete both copied functions. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L138-L149", + "text": "| [`v1/worker/attention_model_runner.py`](../../../afd_plugin/v1/worker/attention_model_runner.py) | Disables vLLM 0.26's block-table-only Attention metadata cache only while multiple DBO ubatches are built, then restores every affected builder in `finally`. Remove this workaround when [vLLM #48659](https://github.com/vllm-project/vllm/pull/48659), or an equivalent ubatch-aware cache key, reaches the pinned release. |\n\nSeveral scoped upstream adaptations live with their semantic owners rather\nthan this patch directory. Model dummy runs and V2 execution temporarily wrap\n`create_forward_context`; CUDA worker startup temporarily substitutes the V1\nor V2 runner symbol; V2 graph capture temporarily wraps native input\npreparation; and full-graph replay installs an instance-local manager hook.\nEvery scope restores symbols and runner state in `finally`. Connector process\ngroups also call private PyTorch/vLLM helpers as described in\n[connector contracts](connector_contracts.md). All of these seams must be\nincluded in an upstream upgrade audit even though they are not process-global\nimport-time patches." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L167-L169", + "text": "When the pinned upstream version changes, copied bodies must be refreshed from\nthat version before reapplying marked AFD deltas. Passing unit tests against a\ndifferent signature is not sufficient compatibility evidence." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L197-L209", + "text": "- `PATCH-INV-003`: unsupported upstream versions are not treated as supported\n merely because best-effort registration continued.\n- `PATCH-INV-004`: patch initialization is idempotent or explicitly guarded,\n and partial application is observable before the affected runtime executes.\n\n## Upstream relationship and validation requirements\n\nPatch signatures and copied code must match their pinned upstream symbols.\nEvery modification requires focused tests for AFD and non-AFD behavior plus\nthe architectural review required by `AGENTS.md`. Version guard changes also\nrequire package compatibility tests. Ascend patch refreshes require an exact\nvLLM-Ascend source reference and the NPU unit/E2E evidence for the affected\npath." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L270-L273", + "text": "The common process-group helper creates a plugin-owned group while temporarily\nswitching PyTorch's default group so vLLM parallel-state helpers initialize\nagainst it. It currently calls private PyTorch/vLLM symbols and is therefore an\nupgrade-sensitive compatibility boundary, not a third-party process-group API." + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/compatibility/rules.md": "---\ntitle: \"AFD compatibility 规则\"\ncreated: 2026-08-06\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin, components, compatibility, review]\nsources:\n - \"afd-plugin@a432692:AGENTS.md\"\n - \"afd-plugin@a432692:afd_plugin/compat/**\"\n - \"afd-plugin@a432692:docs/design/module/compatibility_and_patches.md\"\nconfidence: high\n---\n\n# AFD compatibility 规则\n\n这些规则从 [仓库规则](../../rules.md) 迁入最近 owner,保留原有稳定 ID。\n\n## AFD-3a — patch 必须 upstream-first 且可移除\n\n触发:新增 patch、修改已有 patch 或升级 vLLM/vLLM-Ascend。\n\n- 必须记录精确 upstream file/symbol/version/signature,优先复制对应版本函数,并保持签名和返回类型。\n- AFD 差异必须用 `# ### PATCH START: ...` / `# ### PATCH END: ...` 标出;函数上方必须说明 patch reason、行为变化和 upstream/移除条件。\n- 禁止在便利的全局 hook 中放置本应属于 AFD model、worker 或 connector 的功能。\n- 验收:AFD 和 non-AFD 分支、初始化/失败/shutdown 及需要时的 reload/idempotence 都有 focused tests,并写明移除路径。\n\n## AFD-3b — 上游 API drift 必须直接暴露\n\n触发:访问 vLLM/vLLM-Ascend 结构、添加 fallback 或选择 original-function delegation。\n\n- 必须直接访问预期上游函数和字段,让静态检查和原始错误暴露缺失/改名。\n- 禁止用宽泛 `getattr`、`hasattr`、`Any`、`object` 或 `_original_*` delegation 掩盖 drift。只有 `AGENTS.md` 明确允许的超大/不适合内联函数例外才可 delegation,且必须就地解释。\n- 验收:当前 pinned signature 对齐,故意缺失字段不会被 fallback 吞掉,保存的 original 不会在 reload 时被 wrapper 覆盖。\n\npatch 生命周期见 [architecture](architecture.md),版本支持声明见 [仓库规则](../../rules.md)。\n\n## AFD-I6 — 临时替换上游符号的代码必须在 `finally` 里还原,并登记到兼容文档\n\n- 这类临时替换包括:包装 `create_forward_context`、在 `Worker.init_device()` 期间替换 runner 符号、包装 V2 capture 的输入准备、安装 full-graph replay hook、给 DBO 临时关闭 metadata cache。它们都必须在 `finally` 里恢复符号和 runner 状态,异常路径也要恢复。\n- 需要有测试覆盖恢复逻辑和错误路径。参考 `test_attention_model_runner.py` 对 DBO 规避代码的覆盖。\n- 新增的这类替换要写进 `docs/design/module/compatibility_and_patches.md` 的 scoped adaptation 列表,否则升级审计会漏掉它。\n- 需要在整个进程生效的替换,应放到 `afd_plugin/compat/patches/`,并走 patch 审查流程。反过来,AFD 自己的功能不要借全局 hook 藏进 patch 模块。\n- 保存了原函数的 patch,在模块重载时不能用自己的 wrapper 覆盖已保存的原函数。\n\n\n\n## AFD-I5 — 升级 vLLM 或 vLLM-Ascend 基线时,需要同步修改多个文件并做完整审计\n\n- vLLM 目标版本出现在三个地方:`pyproject.toml` 的 extra pin、`afd_plugin/compat/vllm.py` 的版本检查、README(Overview、Known gaps、GPU installation)。三处必须一致。\n- 插件注册用 `strict=False` 调版本检查,版本不符只告警。不能把“注册成功”当作新版本受支持的证据(`PATCH-INV-003`)。\n - `engine_core.py` 和 `npu/force_load_balance.py` 没有 patch 内的版本守卫,只能靠 review 把关。\n - `async_dp_engine.py` 里复制的 coordinator 只在 `0.26.0` 精确版本下生效,守卫要跟着更新。\n - `attention_model_runner.py` 里规避 DBO metadata cache 的代码:等 vLLM #48659 进入 pin 的版本后删除。\n - 连接器进程组用到的私有 PyTorch 和 vLLM 符号。\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。刷新 Ascend 侧时,必须引用确切的 vLLM-Ascend 源码 commit(当前是 `80d8c194f`),同步 README 的 Ascend 基线表,并附上 NPU 单元测试和 E2E 证据。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I5", + "note": "kb init skeleton: judge said pass; on main the text is kept", + "rule_id": "AFD-I5", + "source": "owner", + "stage": "skeleton", + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I6.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I6.json new file mode 100644 index 00000000..06637d99 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I6.json @@ -0,0 +1,52 @@ +{ + "base": { + "repos/afd-plugin/components/compatibility/rules.md": "---\ntitle: \"AFD compatibility 规则\"\ncreated: 2026-08-06\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin, components, compatibility, review]\nsources:\n - \"afd-plugin@a432692:AGENTS.md\"\n - \"afd-plugin@a432692:afd_plugin/compat/**\"\n - \"afd-plugin@a432692:docs/design/module/compatibility_and_patches.md\"\nconfidence: high\n---\n\n# AFD compatibility 规则\n\n这些规则从 [仓库规则](../../rules.md) 迁入最近 owner,保留原有稳定 ID。\n\n## AFD-3a — patch 必须 upstream-first 且可移除\n\n触发:新增 patch、修改已有 patch 或升级 vLLM/vLLM-Ascend。\n\n- 必须记录精确 upstream file/symbol/version/signature,优先复制对应版本函数,并保持签名和返回类型。\n- AFD 差异必须用 `# ### PATCH START: ...` / `# ### PATCH END: ...` 标出;函数上方必须说明 patch reason、行为变化和 upstream/移除条件。\n- 禁止在便利的全局 hook 中放置本应属于 AFD model、worker 或 connector 的功能。\n- 验收:AFD 和 non-AFD 分支、初始化/失败/shutdown 及需要时的 reload/idempotence 都有 focused tests,并写明移除路径。\n\n## AFD-3b — 上游 API drift 必须直接暴露\n\n触发:访问 vLLM/vLLM-Ascend 结构、添加 fallback 或选择 original-function delegation。\n\n- 必须直接访问预期上游函数和字段,让静态检查和原始错误暴露缺失/改名。\n- 禁止用宽泛 `getattr`、`hasattr`、`Any`、`object` 或 `_original_*` delegation 掩盖 drift。只有 `AGENTS.md` 明确允许的超大/不适合内联函数例外才可 delegation,且必须就地解释。\n- 验收:当前 pinned signature 对齐,故意缺失字段不会被 fallback 吞掉,保存的 original 不会在 reload 时被 wrapper 覆盖。\n\npatch 生命周期见 [architecture](architecture.md),版本支持声明见 [仓库规则](../../rules.md)。\n\n## AFD-I5 — 升级 vLLM 或 vLLM-Ascend 基线时,需要同步修改多个文件并做完整审计\n\n- vLLM 目标版本出现在三个地方:`pyproject.toml` 的 extra pin、`afd_plugin/compat/vllm.py` 的版本检查、README(Overview、Known gaps、GPU installation)。三处必须一致。\n- 插件注册用 `strict=False` 调版本检查,版本不符只告警。不能把“注册成功”当作新版本受支持的证据(`PATCH-INV-003`)。\n - `engine_core.py` 和 `npu/force_load_balance.py` 没有 patch 内的版本守卫,只能靠 review 把关。\n - `async_dp_engine.py` 里复制的 coordinator 只在 `0.26.0` 精确版本下生效,守卫要跟着更新。\n - `attention_model_runner.py` 里规避 DBO metadata cache 的代码:等 vLLM #48659 进入 pin 的版本后删除。\n - 连接器进程组用到的私有 PyTorch 和 vLLM 符号。\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。刷新 Ascend 侧时,必须引用确切的 vLLM-Ascend 源码 commit(当前是 `80d8c194f`),同步 README 的 Ascend 基线表,并附上 NPU 单元测试和 E2E 证据。\n\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L55-L57", + "text": "Compatibility code consumes the common plugin boundary and adapts pinned\nupstream behavior for AFD roles. Role modules may rely on those adaptations;\npatch modules must not become a general home for AFD-owned functionality." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L82-L82", + "text": "| CUDA DBO Attention metadata workaround | [`attention_model_runner.py`](../../../afd_plugin/v1/worker/attention_model_runner.py) | restoration and error-path coverage in [`test_attention_model_runner.py`](../../../tests/unit/v1/worker/test_attention_model_runner.py) |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L138-L149", + "text": "| [`v1/worker/attention_model_runner.py`](../../../afd_plugin/v1/worker/attention_model_runner.py) | Disables vLLM 0.26's block-table-only Attention metadata cache only while multiple DBO ubatches are built, then restores every affected builder in `finally`. Remove this workaround when [vLLM #48659](https://github.com/vllm-project/vllm/pull/48659), or an equivalent ubatch-aware cache key, reaches the pinned release. |\n\nSeveral scoped upstream adaptations live with their semantic owners rather\nthan this patch directory. Model dummy runs and V2 execution temporarily wrap\n`create_forward_context`; CUDA worker startup temporarily substitutes the V1\nor V2 runner symbol; V2 graph capture temporarily wraps native input\npreparation; and full-graph replay installs an instance-local manager hook.\nEvery scope restores symbols and runner state in `finally`. Connector process\ngroups also call private PyTorch/vLLM helpers as described in\n[connector contracts](connector_contracts.md). All of these seams must be\nincluded in an upstream upgrade audit even though they are not process-global\nimport-time patches." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L182-L183", + "text": "- A patch that saves an original callable must never overwrite that saved\n original with its own wrapper on reload." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L322-L327", + "text": "V2 rejects DBO and ubatching. For full-graph capture, the V2 runner wraps the\nnative capture input-preparation seam and publishes exactly one warmup and one\ncapture payload for every native descriptor before the graph body. Native\nfull-graph replay does not create a `ForwardContext`, so an execute-scoped\nmanager hook publishes the padded control shape immediately before replay.\nAll temporary hooks and pending metadata state are restored in `finally`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L132-L137", + "text": "Both CUDA workers delegate device and distributed setup to native\n`Worker.init_device()`. During that synchronous construction window, they\ntemporarily replace the selected upstream runner symbol with the AFD runner\nand restore it in `finally`. V1 Attention selects `AFDAttentionModelRunner`;\nV2 selects `AFDAttentionModelRunnerV2`; a V2-paired FFN still selects the\nminimal `GPUFFNModelRunner`. `torch.accelerator.empty_cache()` releases" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L158-L164", + "text": "ModelRunnerV2 rejects ubatching and reuses the native full-graph manager. Its\nAttention wrapper observes each native capture descriptor at the exact input\npreparation seam, publishes a warmup and capture payload outside the graph,\nand suppresses duplicate context sends. Full-graph replay bypasses context\ncreation, so an instance-scoped `run_fullgraph` hook publishes the padded\nshape immediately before replay. The wrapper verifies descriptor order and\ncall count and restores all temporary symbols and state in `finally`." + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/components/compatibility/rules.md": "---\ntitle: \"AFD compatibility 规则\"\ncreated: 2026-08-06\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin, components, compatibility, review]\nsources:\n - \"afd-plugin@a432692:AGENTS.md\"\n - \"afd-plugin@a432692:afd_plugin/compat/**\"\n - \"afd-plugin@a432692:docs/design/module/compatibility_and_patches.md\"\nconfidence: high\n---\n\n# AFD compatibility 规则\n\n这些规则从 [仓库规则](../../rules.md) 迁入最近 owner,保留原有稳定 ID。\n\n## AFD-3a — patch 必须 upstream-first 且可移除\n\n触发:新增 patch、修改已有 patch 或升级 vLLM/vLLM-Ascend。\n\n- 必须记录精确 upstream file/symbol/version/signature,优先复制对应版本函数,并保持签名和返回类型。\n- AFD 差异必须用 `# ### PATCH START: ...` / `# ### PATCH END: ...` 标出;函数上方必须说明 patch reason、行为变化和 upstream/移除条件。\n- 禁止在便利的全局 hook 中放置本应属于 AFD model、worker 或 connector 的功能。\n- 验收:AFD 和 non-AFD 分支、初始化/失败/shutdown 及需要时的 reload/idempotence 都有 focused tests,并写明移除路径。\n\n## AFD-3b — 上游 API drift 必须直接暴露\n\n触发:访问 vLLM/vLLM-Ascend 结构、添加 fallback 或选择 original-function delegation。\n\n- 必须直接访问预期上游函数和字段,让静态检查和原始错误暴露缺失/改名。\n- 禁止用宽泛 `getattr`、`hasattr`、`Any`、`object` 或 `_original_*` delegation 掩盖 drift。只有 `AGENTS.md` 明确允许的超大/不适合内联函数例外才可 delegation,且必须就地解释。\n- 验收:当前 pinned signature 对齐,故意缺失字段不会被 fallback 吞掉,保存的 original 不会在 reload 时被 wrapper 覆盖。\n\npatch 生命周期见 [architecture](architecture.md),版本支持声明见 [仓库规则](../../rules.md)。\n\n## AFD-I5 — 升级 vLLM 或 vLLM-Ascend 基线时,需要同步修改多个文件并做完整审计\n\n- vLLM 目标版本出现在三个地方:`pyproject.toml` 的 extra pin、`afd_plugin/compat/vllm.py` 的版本检查、README(Overview、Known gaps、GPU installation)。三处必须一致。\n- 插件注册用 `strict=False` 调版本检查,版本不符只告警。不能把“注册成功”当作新版本受支持的证据(`PATCH-INV-003`)。\n - `engine_core.py` 和 `npu/force_load_balance.py` 没有 patch 内的版本守卫,只能靠 review 把关。\n - `async_dp_engine.py` 里复制的 coordinator 只在 `0.26.0` 精确版本下生效,守卫要跟着更新。\n - `attention_model_runner.py` 里规避 DBO metadata cache 的代码:等 vLLM #48659 进入 pin 的版本后删除。\n - 连接器进程组用到的私有 PyTorch 和 vLLM 符号。\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。刷新 Ascend 侧时,必须引用确切的 vLLM-Ascend 源码 commit(当前是 `80d8c194f`),同步 README 的 Ascend 基线表,并附上 NPU 单元测试和 E2E 证据。\n\n\n\n## AFD-I6 — 临时替换上游符号的代码必须在 `finally` 里还原,并登记到兼容文档\n\n- 这类临时替换包括:包装 `create_forward_context`、在 `Worker.init_device()` 期间替换 runner 符号、包装 V2 capture 的输入准备、安装 full-graph replay hook、给 DBO 临时关闭 metadata cache。它们都必须在 `finally` 里恢复符号和 runner 状态,异常路径也要恢复。\n- 需要有测试覆盖恢复逻辑和错误路径。参考 `test_attention_model_runner.py` 对 DBO 规避代码的覆盖。\n- 新增的这类替换要写进 `docs/design/module/compatibility_and_patches.md` 的 scoped adaptation 列表,否则升级审计会漏掉它。\n- 需要在整个进程生效的替换,应放到 `afd_plugin/compat/patches/`,并走 patch 审查流程。反过来,AFD 自己的功能不要借全局 hook 藏进 patch 模块。\n- 保存了原函数的 patch,在模块重载时不能用自己的 wrapper 覆盖已保存的原函数。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I6", + "note": "kb init skeleton: judge said unsure; on main the text is kept", + "rule_id": "AFD-I6", + "source": "owner", + "stage": "skeleton", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I7.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I7.json new file mode 100644 index 00000000..6e0336c4 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I7.json @@ -0,0 +1,57 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L40-L41", + "text": "- DeepSeek MoE handoff at the remote-experts boundary on CUDA, with the gate\n placed on either Attention or FFN." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L48-L48", + "text": "| Qwen3 MoE | `Qwen3MoeForCausalLM` | `AFDQwen3MoeForCausalLM` | CUDA with `compute_gate_on_attention=false`. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L73-L80", + "text": "- Qwen3 MoE currently rejects Attention-side gate placement, sequence-parallel\n MoE, EPLB, pipeline parallelism, speculative decoding, LoRA, and NPU.\n- Qwen3.5/Qwen3.6 MoE adapter-family support is limited to the CUDA text-only\n lane; repository hardware E2E evidence currently covers Qwen3.6-35B-A3B.\n NPU and multimodal execution are unsupported/unverified;\n `compute_gate_on_attention=true`, pipeline parallelism, asynchronous and\n multi-node execution are unsupported; quantization is unverified; no\n performance claim is made." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L95-L102", + "text": "| `CAMP2pAFDConnector` | `core_num`, optional `attn_core_num` / `ffn_core_num`, `compute_gate_on_attention`, and `quant_mode`. Core counts must be positive; the current runtime rejects gate-on-Attention and any nonzero quantization mode. |\n| `CAMAsyncAFDConnector` | `dynamicQuant`, `attn_ranks_per_dp`, `async_moe_ubatching`, `async_moe_num_ubatches`, and `async_moe_split`. Runtime validation further limits dynamic quantization and the optional request/token stage pipeline. |\n\nThe common `compute_gate_on_attention` field remains on `AFDConfig` and is the\nmodel-routing selector. CUDA P2P supports both values. CAMP2P also parses a\nconnector-local field with that name for its operator contract, but the current\nsynchronous NPU runtime requires both common and connector-local values to be\n`false`. CAM async requires the common field to be `true`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L150-L153", + "text": "| Constraint | CUDA V2 | Ascend V2 |\n| --- | --- | --- |\n| Connector | Synchronous `P2pNcclAFDConnector` | Synchronous `CAMP2pAFDConnector` |\n| Gate placement | `compute_gate_on_attention=false` | `compute_gate_on_attention=false` |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L279-L288", + "text": "The documented `CAMAsyncAFDConnector` configuration uses\n`compute_gate_on_attention=true`. Attention computes MoE routing data and sends\nhidden states together with `topk_weights`, `topk_ids`, and optional\n`router_logits`. FFN receive is delayed until the next layer when possible,\nwhich permits CAM dispatch/combine overlap. Dense layers remain local to the\nAttention model path.\n\nThe optional `async_moe_ubatching` mode is distinct from vLLM native DBO. It:\n\n- requires `compute_gate_on_attention=true`;" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L141-L147", + "text": "- Both workers run the same V2 topology and feature validator as the paired\n Attention role before constructing communication resources.\n\nThe V2 pair therefore requires the synchronous platform connector,\n`compute_gate_on_attention=false`, PP/PCP/DCP size 1, configured role ranks\nequal to DP x TP, static EP, a registered AFD model, and no DBO or ubatching.\nCUDA uses `P2pNcclAFDConnector`; Ascend uses `CAMP2pAFDConnector`. These limits" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L340-L344", + "text": "| CUDA V1 + `P2pNcclAFDConnector` | Eager or `FULL_DECODE_ONLY` CUDA Graph | Native DBO, exactly two ubatches | Registered CUDA model boundaries; Attention-side or FFN-side gate where the model supports it | DeepSeek-V2-Lite eager/graph/DBO accuracy E2E; model, graph, connector, and profiler unit tests |\n| CUDA V2 + `P2pNcclAFDConnector` | Eager or `FULL_DECODE_ONLY` native V2 CUDA Graph | DBO and ubatching rejected | `compute_gate_on_attention=false`; PP/CP, elastic EP, EPLB, SP MoE, and compile SP rejected; role ranks equal DP x TP | DeepSeek-V2-Lite eager/graph DP2 and TP2 accuracy E2E, plus focused V2 unit tests |\n| Ascend V1 + `CAMP2pAFDConnector` | Eager or current ACL Graph path | Native DBO, exactly two ubatches | Common and connector-local `compute_gate_on_attention=false`; `connector_extra_config.quant_mode=0`; plugin CANN ops required | Backend-neutral DeepSeek-V2-Lite eager/graph/DBO accuracy cases plus NPU runtime, graph, ops, connector, and profiler unit tests |\n| Ascend V2 + `CAMP2pAFDConnector` | Eager, `FULL`, or `FULL_DECODE_ONLY` native V2 ACL Graph | DBO and ubatching rejected | `compute_gate_on_attention=false`; PP/CP, elastic EP, EPLB, SP MoE, and compile SP rejected; role ranks equal DP x TP | Focused runner, context, validation, and device-contract unit tests; no repository hardware E2E case |\n| Ascend + `CAMAsyncAFDConnector` | Eager only | Native DBO rejected; optional AFD-managed MoE ubatching uses exactly two request or token-balanced stages | Experimental v0.26 port; `async=true`; documented path uses common `compute_gate_on_attention=true`; token mode requires Attention TP > 1; model runner v1 PCP is unsupported; prefill and decode context parallelism are unsupported; `connector_extra_config.dynamicQuant` is 0 or 1; plugin-owned async CAM ops required | Focused unit coverage; pre-fix DP3TP2/EP2 six-case E2E matrix; post-fix full 61-layer DP2TP8+EP16 token-split run reached `0.9522` strict match on the complete GSM8K evaluation |" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I7", + "note": "kb init skeleton: judge said unsure; on main the text is kept", + "rule_id": "AFD-I7", + "source": "owner", + "stage": "skeleton", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I8.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I8.json new file mode 100644 index 00000000..beee5321 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I8.json @@ -0,0 +1,62 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L58-L58", + "text": "| `CAMP2pAFDConnector` | Ascend NPU | Decode | Sync | `FULL_DECODE_ONLY` ACL graph | Uses HCCL/CAMP2P custom ops. Ascend ops build by default on NPU platforms. See the [synchronous DeepSeek V3.2 recipe](recipe/npu/CAMP2pAFDConnector/deepseek_v3_2/README.md). |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L68-L70", + "text": "- vLLM/vLLM-Ascend model runner v2 is not supported.\n- GPU and NPU E2E tests are opt-in and require real hardware plus model weights.\n- GPU CUDA graph support is limited to `FULL_DECODE_ONLY`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L180-L185", + "text": "GPU model runner v2 is not supported. Select model runner v1 before starting\neither GPU role:\n\n```bash\nexport VLLM_USE_V2_MODEL_RUNNER=0\n```" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L139-L162", + "text": "## ModelRunnerV2 boundary\n\nModelRunnerV2 is an upstream runtime selection, not an AFD configuration key.\nOn Attention, the V2 runners are thin subclasses of the native CUDA or Ascend\nrunner: native V2 retains request state, input preparation, Attention/KV\nexecution, sampling, and output ownership. `AFDMetadataProviderMixin` adds the\nconnector-facing sidecar, DP control payload, transaction IDs, and capture\nmetadata without porting V1 execution methods.\n\nThe supported V2 deployment is deliberately narrower than V1:\n\n| Constraint | CUDA V2 | Ascend V2 |\n| --- | --- | --- |\n| Connector | Synchronous `P2pNcclAFDConnector` | Synchronous `CAMP2pAFDConnector` |\n| Gate placement | `compute_gate_on_attention=false` | `compute_gate_on_attention=false` |\n| Parallelism | PP=PCP=DCP=1; configured role ranks equal DP x TP; static EP enabled | Same |\n| Excluded features | Elastic EP, EPLB, sequence-parallel MoE, compile SP, DBO, and ubatching | Same |\n| Graph execution | Eager or `FULL_DECODE_ONLY` | Eager, `FULL`, or `FULL_DECODE_ONLY` |\n| Model | Must resolve to a registered AFD architecture | Same |\n\nBoth roles validate the paired V2 topology, but only Attention uses the native\nV2 model runner. FFN remains connector-driven and uses its existing AFD runner\nsurface. CUDA V2 has DP2/TP2 eager and graph E2E evidence; the current Ascend\nV2 contract has focused unit evidence but no repository hardware E2E case." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L129-L148", + "text": "## ModelRunnerV2 pairing\n\nFFN remains connector-driven when the upstream configuration selects\nModelRunnerV2. It does not adopt native V2 request state or scheduler-driven\nexecution:\n\n- CUDA temporarily substitutes `GPUFFNModelRunner` at the native V2\n construction seam inside `Worker.init_device()`; the existing AFD runner is\n still the runtime object.\n- Ascend always constructs `AFDNPUFFNModelRunner`. When V2 is selected, its\n connector-driven forward enters the upstream MRV2 forward-context shape so\n Ascend-specific state is installed in `additional_kwargs`.\n- Both workers run the same V2 topology and feature validator as the paired\n Attention role before constructing communication resources.\n\nThe V2 pair therefore requires the synchronous platform connector,\n`compute_gate_on_attention=false`, PP/PCP/DCP size 1, configured role ranks\nequal to DP x TP, static EP, a registered AFD model, and no DBO or ubatching.\nCUDA uses `P2pNcclAFDConnector`; Ascend uses `CAMP2pAFDConnector`. These limits\ndescribe the pair even though only Attention inherits a native V2 runner." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/e2e_testing.md:L105-L110", + "text": "| `afd-v2-eager-dp2` | CUDA ModelRunnerV2 eager, Attention DP2 + FFN DP2 | 4 | V2 data-parallel lifecycle and accuracy. |\n| `afd-v2-graph-dp2` | CUDA ModelRunnerV2 graph, Attention DP2 + FFN DP2 | 4 | V2 data-parallel full-decode graph path. |\n| `afd-v2-eager-tp2` | CUDA ModelRunnerV2 eager, Attention TP2 + FFN TP2 | 4 | V2 tensor-parallel lifecycle and accuracy. |\n| `afd-v2-graph-tp2` | CUDA ModelRunnerV2 graph, Attention TP2 + FFN TP2 | 4 | V2 tensor-parallel full-decode graph path. |\n\nCUDA CI runs the legacy and ModelRunnerV2 matrices as separate jobs with a" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/e2e_testing.md:L147-L151", + "text": "The ModelRunnerV2 matrix is CUDA-only in the current E2E harness. Its local\n1A1F cases are `afd-v2-eager-1a1f` and `afd-v2-graph-1a1f`; CI selects the four\nDP2/TP2 cases listed above by exact node ID on `l4_4`. The Ascend\nModelRunnerV2 implementation currently has focused unit evidence but no E2E\ncase, so it is not included in the hardware gate." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L281-L285", + "text": "The NPU V2 runner supports eager, `FULL`, and `FULL_DECODE_ONLY`. Like CUDA V2,\nit publishes descriptor-matched warmup/capture control outside formal graph\ncapture and installs an instance-scoped pre-replay hook because native full\nreplay creates no `ForwardContext`. V2 does not use `AscendUBatchWrapper` and\nrejects DBO/ubatching." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L341-L348", + "text": "| CUDA V2 + `P2pNcclAFDConnector` | Eager or `FULL_DECODE_ONLY` native V2 CUDA Graph | DBO and ubatching rejected | `compute_gate_on_attention=false`; PP/CP, elastic EP, EPLB, SP MoE, and compile SP rejected; role ranks equal DP x TP | DeepSeek-V2-Lite eager/graph DP2 and TP2 accuracy E2E, plus focused V2 unit tests |\n| Ascend V1 + `CAMP2pAFDConnector` | Eager or current ACL Graph path | Native DBO, exactly two ubatches | Common and connector-local `compute_gate_on_attention=false`; `connector_extra_config.quant_mode=0`; plugin CANN ops required | Backend-neutral DeepSeek-V2-Lite eager/graph/DBO accuracy cases plus NPU runtime, graph, ops, connector, and profiler unit tests |\n| Ascend V2 + `CAMP2pAFDConnector` | Eager, `FULL`, or `FULL_DECODE_ONLY` native V2 ACL Graph | DBO and ubatching rejected | `compute_gate_on_attention=false`; PP/CP, elastic EP, EPLB, SP MoE, and compile SP rejected; role ranks equal DP x TP | Focused runner, context, validation, and device-contract unit tests; no repository hardware E2E case |\n| Ascend + `CAMAsyncAFDConnector` | Eager only | Native DBO rejected; optional AFD-managed MoE ubatching uses exactly two request or token-balanced stages | Experimental v0.26 port; `async=true`; documented path uses common `compute_gate_on_attention=true`; token mode requires Attention TP > 1; model runner v1 PCP is unsupported; prefill and decode context parallelism are unsupported; `connector_extra_config.dynamicQuant` is 0 or 1; plugin-owned async CAM ops required | Focused unit coverage; pre-fix DP3TP2/EP2 six-case E2E matrix; post-fix full 61-layer DP2TP8+EP16 token-split run reached `0.9522` strict match on the complete GSM8K evaluation |\n\nAll rows target vLLM 0.26.0. Hardware validation exists for CUDA V1/V2 and the\nrecorded Ascend V1 paths; the Ascend V2 row is an implemented, unit-tested\ncontract rather than a hardware-validated claim. GPU/NPU rank topology and" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I8", + "note": "kb init skeleton: judge said pass; on main the text is kept", + "rule_id": "AFD-I8", + "source": "owner", + "stage": "skeleton", + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I9.json b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I9.json new file mode 100644 index 00000000..abaa25fc --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-good-skeleton-AFD-I9.json @@ -0,0 +1,42 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L209-L242", + "text": "Attention remains request-driven even when the selected connector makes FFN\nconnector-driven. Idle DP ranks use the upstream dummy-batch path; the CUDA\nrunner installs AFD metadata lazily for that path because native dummy\nexecution bypasses `_model_forward()`.\n\n## Forward-context metadata\n\nThe canonical metadata location on both CUDA and Ascend is:\n\n```python\nforward_context.additional_kwargs[\"afd_metadata\"]\n```\n\nThe installed object is `AFDForwardContextMetadata`. For the current model\npath it carries stage token/request starts, padded and unpadded token lengths,\nstage count/index, a transaction id, and the connector reference consumed by\nplugin-owned model code. These fields describe the current implementation;\nthe open metadata issues prevent the object shape and live connector reference\nfrom becoming a long-term API.\n\nV1 creates pending AFD metadata while native Attention metadata is built and\ninstalls it immediately before model forward. V2 temporarily installs a\nprovider at the native `ForwardContext` creation seam and attaches the same\nsidecar to the context created by the upstream runner. The shared metadata\nmixin supplies the ordinary single-stage and graph-control behavior. V1\nnative ubatching still gives each child context stage-local metadata.\n\nWhen vLLM does not produce `DPMetadata` for DP size 1, the shared provider\ncreates `AFDDPMetadata` from the pending stage token count. DP greater than 1\nrequires native DP metadata on the ordinary request path.\n\nThe previous NPU design described a separate forward-context metadata mirror.\nThat is no longer the contract: current CUDA and Ascend model paths read the\ncanonical `additional_kwargs` entry." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L229-L233", + "text": "4. Receive an `AFDA2FTransferPayload` for the current layer/stage, which\n carries the hidden states plus an `AFDTransferContext` (transfer metadata\n and the backend `AFDTransferState`).\n5. Install stage DP metadata and transfer metadata at\n `ForwardContext.additional_kwargs[\"afd_metadata\"]`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L217-L229", + "text": "### NPU forward context\n\nAttention extends the upstream vLLM-Ascend forward flow and installs AFD data\nin `ForwardContext.additional_kwargs`. FFN uses\n`ascend_forward_context()` to create the minimal upstream context needed for\nconnector-driven MoE compute, including token counts and ACL graph runtime\nmode when applicable.\n\nWhen ModelRunnerV2 is selected, `ascend_forward_context()` uses native\n`set_forward_context()` plus vLLM-Ascend's MRV2 profile override. This places\nAscend-specific state, `model_instance`, and AFD metadata in\n`additional_kwargs`, matching the proxy layout read by the V2 runtime. V1\ncontinues to use `set_ascend_forward_context()`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L136-L136", + "text": "| [`compat/npu/forward_context.py`](../../../afd_plugin/compat/npu/forward_context.py) | Enters the pinned Ascend forward context for connector-driven FFN compute and installs AFD metadata in `additional_kwargs`. V1 uses `set_ascend_forward_context`; a V2-paired FFN uses native `set_forward_context` plus the Ascend MRV2 profile override so platform state follows the V2 proxy layout. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L169-L169", + "text": "| `AFDForwardContextMetadata` | Stage/slice information and a live connector reference used by model execution. | **Draft**; model-facing ownership is unresolved. |" + } + ], + "expected": "pass", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n" + }, + "id": "init-good-skeleton-AFD-I9", + "note": "kb init skeleton: judge said unsure; on main the text is kept", + "rule_id": "AFD-I9", + "source": "owner", + "stage": "skeleton", + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-broken_path-skeleton-AFD-I1.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-broken_path-skeleton-AFD-I1.json new file mode 100644 index 00000000..30013340 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-broken_path-skeleton-AFD-I1.json @@ -0,0 +1,44 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L61-L63", + "text": "Connector implementations are grouped by backend package:\n`afd_plugin.connectors.gpu` for GPU-only connectors,\n`afd_plugin.connectors.npu` for NPU-only connectors." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L274-L281", + "text": "`role` must be `attention` or `ffn`. `connector` must be `P2pNcclAFDConnector`,\n`CAMP2pAFDConnector`, or `CAMAsyncAFDConnector`. AFD is active when\n`additional_config[\"afd\"]` is present and passes common AFD config validation;\nomit `additional_config[\"afd\"]` to disable AFD. Connector-owned\n`connector_extra_config` is strictly validated by the selected connector parser\nwhen the connector/runtime is constructed. The plugin also accepts selected\ncompatibility aliases such as `afd_role`, `afd_connector`, `afd_host`, and\n`afd_port`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L67-L85", + "text": "`AFDConnectorFactory` stores lazy loaders, so importing the factory does not\nimport CUDA or Ascend implementations. Built-in names are registered at module\nload time. `create_connector(rank, local_rank, vllm_config, afd_config=None)`\nparses configuration when needed, resolves the runtime role rank from the\nworker's global DP rank and local PCP/TP coordinates, and constructs the\nselected class. Every connector receives that resolved role rank and is only\nresponsible for mapping it into its backend-specific world rank. A loader must\nresolve to an `AFDConnectorBase` subclass; duplicate registration is rejected\nunless `replace=True`, and unknown names fail before resource initialization.\n\nThe registry method is a usable implementation hook, but the complete public\nextension contract is **draft**. Configuration still rejects names outside its\nseparate hard-coded allow-list, so factory registration alone is insufficient\nfor an external connector.\n\nEach connector class implements `parse_extra_config()` and construction stores\nthe resulting immutable `ConnectorExtraInfo` as `extra_info`. The factory can\nalso parse this information without constructing communication resources,\nwhich lets feature validation use the same connector-owned schema." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L332-L339", + "text": "The `AFDControlPlane` split removes control-plane methods from\n`AFDConnectorBase`, so CAM async no longer implements semantic no-ops. Its\nconnector-driven work-item methods still live outside the abstract base. The\nconfiguration allow-list is not derived from the factory registry, and\n`AFDA2FTransferPayload` combines common data with backend state.\nConnector-owned typed configuration implements the decision from\n[#89](https://github.com/JiusiServe/afd-plugin/issues/89), but it does not by\nitself make factory registration a public extension contract." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L135-L135", + "text": "| [`compat/npu/feature_validation.py`](../../../afd_plugin/compat/npu/feature_validation.py) | Parses connector-owned typed extra information through the factory and fails before execution for unsupported NPU connector, quantization, graph, DBO, gate, or async MoE combinations. |" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/removed-feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n" + }, + "id": "init-synthetic-broken_path-skeleton-AFD-I1", + "mutation": "broken_path", + "note": "kb init synthetic bad case from good rule AFD-I1: path claim `compat/npu/feature_validation.py` replaced by `compat/npu/removed-feature_validation.py`, which the upstream does not have", + "rule_id": "AFD-I1", + "source": "synthetic", + "stage": "skeleton", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I22.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I22.json new file mode 100644 index 00000000..2f24871b --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I22.json @@ -0,0 +1,49 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L198-L204", + "text": " def __init__(\n self,\n *,\n config: _DeepseekAdapterConfig,\n layer_idx: int,\n prefix: str,\n vllm_config: VllmConfig," + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L322-L349", + "text": "\n # Patch reason: native DeepSeek constructs both Attention and FFN modules.\n # Patch functionality: construct only the modules owned by the active AFD role.\n # Signature: matches upstream; no added parameters.\n # Upstream: vLLM v0.26.0, vllm/model_executor/models/deepseek_v2.py\n # Commit: 568afb3a13806beb53bb2e6bd518269357b237c0\n def __init__(\n self,\n vllm_config: VllmConfig,\n prefix: str,\n config: DeepseekV2Config | None = None,\n topk_indices_buffer: torch.Tensor | None = None,\n ) -> None:\n # ### PATCH START: require an explicit AFD role before allocation.\n afd_config = parse_afd_config(vllm_config, validate=False)\n afd_role = afd_config.role\n\n torch.nn.Module.__init__(self)\n # ### PATCH END\n\n if config is None:\n config = vllm_config.model_config.hf_config\n model_config = vllm_config.model_config\n cache_config = vllm_config.cache_config\n quant_config = vllm_config.quant_config\n parallel_config = vllm_config.parallel_config\n\n self.hidden_size = config.hidden_size" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L402-L411", + "text": "\n if afd_role == \"attention\":\n attn_cls = (\n native.DeepseekAttention\n if use_mha\n else (\n native.DeepseekV2MLAAttention\n if model_config.use_mla\n else native.DeepseekV2Attention\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L496-L500", + "text": " quant_config=quant_config,\n prefix=f\"{prefix}.mlp\",\n )\n # ### PATCH END\n" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L641-L658", + "text": "\n fall_back_to_pt_during_load = False\n\n # Patch reason: native DeepSeek always creates native Decoder layers.\n # Patch functionality: create role-aware AFD layers without full allocation.\n # Signature: matches upstream; no added parameters.\n # Upstream: vLLM v0.26.0, vllm/model_executor/models/deepseek_v2.py\n # Commit: 568afb3a13806beb53bb2e6bd518269357b237c0\n def __init__(self, *, vllm_config: VllmConfig, prefix: str = \"\"):\n # ### PATCH START: require AFD activation and avoid native allocation.\n afd_config = parse_afd_config(vllm_config, validate=False)\n if bool(\n getattr(\n vllm_config.parallel_config,\n \"use_sequence_parallel_moe\",\n False,\n ),\n ):" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L807-L825", + "text": " llama_4_scaling_config = getattr(self.config, \"llama_4_scaling\", None)\n if llama_4_scaling_config is None:\n return None\n return native._get_llama_4_scaling(\n original_max_position_embeddings=llama_4_scaling_config[\n \"original_max_position_embeddings\"\n ],\n scaling_beta=llama_4_scaling_config[\"beta\"],\n positions=positions,\n )\n\n\nclass AFDDeepseekV2ForCausalLM(native.DeepseekV2ForCausalLM):\n \"\"\"DeepSeekV2 causal LM wrapper for AFD execution.\"\"\"\n\n model_cls = AFDDeepseekV2Model\n\n def __init__(self, *, vllm_config: VllmConfig, prefix: str = \"\") -> None:\n self.afd_config = parse_afd_config(vllm_config, validate=False)" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n" + }, + "id": "init-synthetic-shifted_range-deepen-AFD-I22", + "mutation": "shifted_range", + "note": "kb init synthetic bad case from good rule AFD-I22: evidence moved off the lines the rule describes", + "rule_id": "AFD-I22", + "source": "synthetic", + "stage": "deepen", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I28.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I28.json new file mode 100644 index 00000000..2ed30e2a --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I28.json @@ -0,0 +1,29 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/npu/async_cam_layout.py:L241-L344", + "text": " positions: torch.Tensor,\n llama_4_scaling: torch.Tensor | None,\n metadata: AsyncMoeUbatchMetadata,\n) -> AsyncMoeStageInputs:\n \"\"\"Convert the full model layout into per-stage Attention layouts.\"\"\"\n\n if not metadata.use_sequence_parallel:\n return _build_replicated_stage_inputs(\n hidden_states,\n residual,\n positions,\n llama_4_scaling,\n metadata,\n )\n\n tp_group = get_tp_group()\n tp_rank = int(tp_group.rank_in_group)\n tp_size = int(tp_group.world_size)\n global_input_tokens = metadata.parent_input_tokens\n if tp_size <= 1 or global_input_tokens % tp_size != 0:\n raise ValueError(\n \"Invalid sequence-parallel Async CAM layout: \"\n f\"global_input_tokens={global_input_tokens}, tp_size={tp_size}\",\n )\n local_full_tokens = global_input_tokens // tp_size\n if int(hidden_states.shape[0]) != local_full_tokens:\n raise ValueError(\n \"Async CAM expected a TP-local full-batch hidden-state shard: \"\n f\"expected_rows={local_full_tokens}, \"\n f\"actual_rows={int(hidden_states.shape[0])}\",\n )\n if residual is not None and int(residual.shape[0]) != local_full_tokens:\n raise ValueError(\n \"Async CAM residual must use the same TP-local layout as hidden \"\n f\"states: expected_rows={local_full_tokens}, \"\n f\"actual_rows={int(residual.shape[0])}\",\n )\n\n if residual is None:\n global_hidden_states = _all_gather_rows(\n hidden_states,\n expected_rows=global_input_tokens,\n )\n global_residual = None\n else:\n hidden_width = int(hidden_states.shape[-1])\n residual_width = int(residual.shape[-1])\n combined_states = _all_gather_rows(\n torch.cat((hidden_states, residual), dim=-1),\n expected_rows=global_input_tokens,\n )\n global_hidden_states, global_residual = combined_states.split(\n (hidden_width, residual_width),\n dim=-1,\n )\n positions_token_dim = _require_global_token_dim(\n positions,\n global_input_tokens,\n tensor_name=\"positions\",\n )\n scaling_token_dim = (\n None\n if llama_4_scaling is None\n else _optional_global_token_dim(\n llama_4_scaling,\n global_input_tokens,\n preferred_dim=positions_token_dim,\n )\n )\n\n stage_hidden_states: list[torch.Tensor] = []\n stage_residuals: list[torch.Tensor | None] = []\n stage_positions: list[torch.Tensor] = []\n stage_scaling: list[torch.Tensor | None] = []\n for stage_slice in metadata.stages:\n stage_input_tokens = int(stage_slice.input_tokens)\n if stage_input_tokens % tp_size != 0:\n raise ValueError(\n \"Async CAM stage extent must be TP divisible: \"\n f\"token_slice={stage_slice.token_slice}, tp_size={tp_size}\",\n )\n local_stage_tokens = stage_input_tokens // tp_size\n stage_hidden = _slice_and_pad_token_dim(\n global_hidden_states,\n 0,\n stage_slice.token_slice,\n stage_input_tokens,\n )\n stage_residual = (\n None\n if global_residual is None\n else _slice_and_pad_token_dim(\n global_residual,\n 0,\n stage_slice.token_slice,\n stage_input_tokens,\n )\n )\n stage_position_tensor = _slice_and_pad_token_dim(\n positions,\n positions_token_dim,\n stage_slice.token_slice,\n stage_input_tokens,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/npu/async_cam_layout.py:L539-L550", + "text": " *,\n tensor_name: str,\n) -> None:\n if int(tensor.shape[0]) != expected_tokens:\n raise ValueError(\n f\"Async CAM {tensor_name} must match hidden states on axis 0: \"\n f\"expected_rows={expected_tokens}, actual_rows={int(tensor.shape[0])}\",\n )\n\n\ndef _log_cam_layout(\n phase: str," + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n" + }, + "id": "init-synthetic-shifted_range-deepen-AFD-I28", + "mutation": "shifted_range", + "note": "kb init synthetic bad case from good rule AFD-I28: evidence moved off the lines the rule describes", + "rule_id": "AFD-I28", + "source": "synthetic", + "stage": "deepen", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I37.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I37.json new file mode 100644 index 00000000..6fba2aa5 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I37.json @@ -0,0 +1,44 @@ +{ + "base": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L122-L133", + "text": " )\n return\n\n model = self.model\n if isinstance(model, UBatchWrapper):\n model = model.unwrap()\n self.model = AFDUBatchWrapper(\n model,\n self.vllm_config,\n CUDAGraphMode.NONE,\n self.device,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L551-L557", + "text": " num_ubatches = int(parallel_config.num_ubatches)\n if bool(vllm_config.parallel_config.use_ubatching) and num_ubatches != 2:\n raise RuntimeError(\n \"AFD ubatching currently supports exactly two ubatches; \"\n f\"got num_ubatches={num_ubatches}\",\n )\n" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L318-L324", + "text": " # -> ordinary ModelCudaGraphManager.dispatch() or the forced-eager\n # descriptor -> BatchExecutionDescriptor.num_tokens ->\n # prepare_inputs(...).num_tokens_after_padding. For eager execution,\n # the ordinary no-graph fallback and profile/dummy forced-eager branch\n # both preserve the native batch-descriptor token count.\n batch_descriptor = forward_context.batch_descriptor\n if batch_descriptor is None:" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L347-L350", + "text": " # symbol so each native FULL descriptor publishes one warmup and one capture\n # payload before its forward, while the provider installs the pending AFD\n # sidecar without sending control from inside torch.cuda.graph.\n # Signature: matches vLLM v0.26.0 GPUModelRunner.capture_model exactly." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L427-L436", + "text": " )\n # ### PATCH END: scope AFD metadata provider/replay and profiler step.\n\n # Patch reason: native V2 shutdown does not know about AFD's profiler,\n # connector, or pending metadata sidecar.\n # Patch functionality: preserve delegated native cleanup and guarantee all\n # AFD cleanup layers run when any earlier layer raises.\n # Signature: matches vLLM v0.26.0 GPUModelRunnerV2.shutdown exactly.\n # Upstream source: vllm/v1/worker/gpu/model_runner.py,\n # GPUModelRunner.shutdown; commit" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n" + }, + "id": "init-synthetic-shifted_range-deepen-AFD-I37", + "mutation": "shifted_range", + "note": "kb init synthetic bad case from good rule AFD-I37: evidence moved off the lines the rule describes", + "rule_id": "AFD-I37", + "source": "synthetic", + "stage": "deepen", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I46.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I46.json new file mode 100644 index 00000000..50cc29c2 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I46.json @@ -0,0 +1,34 @@ +{ + "base": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L164-L166", + "text": " )\n return None\n" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L248-L256", + "text": " )\n\n self._ffn_forward(\n dp_metadata_list=dp_metadata_list,\n is_profile=is_profile,\n )\n return None\n\n def _make_graph_key(" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L332-L336", + "text": "\n def capture_model(\n self,\n dp_metadata_list: dict[int, DPMetadata | AFDDPMetadata] | None = None,\n is_warmup: bool = False," + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n" + }, + "id": "init-synthetic-shifted_range-deepen-AFD-I46", + "mutation": "shifted_range", + "note": "kb init synthetic bad case from good rule AFD-I46: evidence moved off the lines the rule describes", + "rule_id": "AFD-I46", + "source": "synthetic", + "stage": "deepen", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I51.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I51.json new file mode 100644 index 00000000..0a81396c --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I51.json @@ -0,0 +1,44 @@ +{ + "base": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_metadata.py:L48-L58", + "text": " if attention_rank < len(expanded_counts)\n else fallback_count\n for attention_rank in subgroup_attention_block(\n ffn_rank, attention_size, ffn_size\n )\n )\n for ffn_rank in range(ffn_size)\n )\n\n\ndef project_ffn_token_counts_to_dp(" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_metadata.py:L37-L52", + "text": " ):\n attention_ranks_per_count = attention_size // len(attention_counts)\n expanded_counts = tuple(\n attention_counts[rank // attention_ranks_per_count]\n for rank in range(attention_size)\n )\n\n # Each FFN rank sums the block that ``build_rank_mapping`` assigns to it.\n return tuple(\n sum(\n max(1, int(expanded_counts[attention_rank]))\n if attention_rank < len(expanded_counts)\n else fallback_count\n for attention_rank in subgroup_attention_block(\n ffn_rank, attention_size, ffn_size\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L286-L302", + "text": " setattr(self, config_name, updated_config)\n\n def reload_weights(self) -> None:\n if self.model is None:\n raise RuntimeError(\"Cannot reload weights before model is loaded\")\n self.load_model()\n\n def _dummy_run(\n self,\n cudagraph_runtime_mode: CUDAGraphMode,\n dp_metadata_list: dict[int, DPMetadata | AFDDPMetadata],\n is_attn_graph_capturing: bool,\n ) -> None:\n mode_name = getattr(cudagraph_runtime_mode, \"name\", str(cudagraph_runtime_mode))\n if mode_name.endswith(\".FULL\"):\n mode_name = \"FULL\"\n" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L632-L644", + "text": " group_size = int(connector.attn_size) // int(connector.ffn_size)\n values = [\n max(1, sum(attention_counts[idx * group_size : (idx + 1) * group_size]))\n for idx in range(int(connector.ffn_size))\n ]\n else:\n values = [max(1, int(fallback))] * int(connector.ffn_size)\n return torch.tensor(values, dtype=torch.int32, device=\"cpu\")\n\n\ndef _ffn_token_count_for_rank(\n connector: AFDConnectorBase,\n num_tokens_across_dp: torch.Tensor," + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L640-L660", + "text": "\n\ndef _ffn_token_count_for_rank(\n connector: AFDConnectorBase,\n num_tokens_across_dp: torch.Tensor,\n) -> int:\n values = _to_int_list(num_tokens_across_dp)\n topology = getattr(connector, \"topology\", None)\n if topology is None:\n raise RuntimeError(\"AFD connector has no initialized topology\")\n role_rank = int(topology.role_rank)\n if role_rank >= len(values):\n return max(1, values[0] if values else 1)\n return max(1, int(values[role_rank]))\n\n\ndef _to_int_list(value: object) -> list[int]:\n if value is None:\n return []\n if isinstance(value, (int, float)):\n return [int(value)]" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n" + }, + "id": "init-synthetic-shifted_range-deepen-AFD-I51", + "mutation": "shifted_range", + "note": "kb init synthetic bad case from good rule AFD-I51: evidence moved off the lines the rule describes", + "rule_id": "AFD-I51", + "source": "synthetic", + "stage": "deepen", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I64.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I64.json new file mode 100644 index 00000000..8f8ea80e --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I64.json @@ -0,0 +1,34 @@ +{ + "base": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L230-L243", + "text": " topk: int,\n expected_tokens: int,\n) -> tuple[torch.Tensor, torch.Tensor]:\n \"\"\"Pack token ids and inert scales for the A2E ids channel.\n\n The operator transports an ``int32`` ids tensor and a ``float32`` scales\n tensor of shape ``(batch, topk)``. The id repeats across the columns, which\n is what the receiving side collapses back. The scales carry no routing\n weight: this channel moves token identity, not router output.\n\n Args:\n input_ids: Token-aligned ids for the local Attention tokens.\n topk: Number of routed experts per token; the operator's column count.\n expected_tokens: Token count the transfer metadata declares." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L823-L851", + "text": ") -> _CAMP2PTopology:\n \"\"\"Calculate the communication rank numbers for one process.\n\n FFN processes come first in the main AFD group, followed by Attention\n processes. All FFN ranks and the first ``min(A, F)`` Attention ranks also\n join the smaller Gloo group that exchanges token counts and batch details.\n\n Args:\n afd_config: Process role and total Attention/FFN rank counts.\n role_rank: This process's runtime number within its own role.\n\n Returns:\n This process's rank numbers and metadata destinations.\n\n Raises:\n ValueError: If the rank counts or this process's role rank are invalid.\n \"\"\"\n attention_size, ffn_size = topology_from_config(afd_config)\n if attention_size <= 0 or ffn_size <= 0:\n raise ValueError(\"CAMP2P topology sizes must be positive\")\n if attention_size < ffn_size:\n raise ValueError(\n \"CAMP2P requires attention_size >= ffn_size, got \"\n f\"{attention_size} < {ffn_size}\",\n )\n if role_rank < 0:\n raise ValueError(f\"CAMP2P role rank must be non-negative, got {role_rank}\")\n\n if afd_config.role == \"attention\":" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L883-L920", + "text": " p2p_rank=p2p_rank,\n attention_size=attention_size,\n ffn_size=ffn_size,\n min_size=min_size,\n dp_metadata_destinations=tuple(destinations),\n )\n\n\ndef _num_tokens_for_ffn_rank(\n dp_metadata_list: dict[int, DPMetadata | AFDDPMetadata],\n stage_idx: int,\n *,\n ffn_rank: int,\n attention_size: int,\n ffn_size: int,\n fallback: int,\n) -> int:\n \"\"\"Count the tokens that one FFN rank will receive from Attention.\n\n An FFN rank may receive data from several consecutive Attention ranks. This\n function adds their token counts. When TP creates several Attention workers\n for one DP rank, it first copies the DP token count to those TP workers.\n\n Args:\n dp_metadata_list: Token counts received from Attention for each ubatch.\n stage_idx: Ubatch number to inspect.\n ffn_rank: FFN rank whose token count is needed.\n attention_size: Total number of Attention ranks.\n ffn_size: Total number of FFN ranks.\n fallback: Value used when the token counts are missing or incomplete.\n\n Returns:\n The number of tokens this FFN rank should receive, always at least one.\n \"\"\"\n dp_metadata = dp_metadata_list[stage_idx]\n if dp_metadata is None:\n return max(1, fallback)\n token_counts = dp_metadata.num_tokens_across_dp_cpu" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n" + }, + "id": "init-synthetic-shifted_range-deepen-AFD-I64", + "mutation": "shifted_range", + "note": "kb init synthetic bad case from good rule AFD-I64: evidence moved off the lines the rule describes", + "rule_id": "AFD-I64", + "source": "synthetic", + "stage": "deepen", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I66.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I66.json new file mode 100644 index 00000000..a2a88124 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-deepen-AFD-I66.json @@ -0,0 +1,44 @@ +{ + "base": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L278-L297", + "text": " expected_tokens: int,\n) -> torch.Tensor:\n \"\"\"Collapse the A2E ids channel back to a token-aligned id vector.\n\n The operator returns ``(N, topk)`` with every column of a row repeating that\n token's id, and sizes ``N`` to the capacity it was given, so this trims to\n the tokens the FFN rank actually computes on.\n\n Args:\n sim_expert_ids: The operator's ids output.\n expected_tokens: Token count derived from the FFN rank's DP metadata,\n which is what the FFN compute will actually run on.\n\n Returns:\n A one-dimensional ``int32`` tensor of length ``expected_tokens``.\n\n Raises:\n ValueError: If fewer ids arrived than the FFN rank computes on. That is\n the misalignment this channel has to catch: a token-keyed router\n would otherwise select experts for the wrong tokens." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L314-L322", + "text": " The connector owns HCCL process-group setup and CAMP2P custom-op transfers.\n Runtime validation rejects unsupported nonzero quantization modes and\n compute-gate-on-attention settings.\n\n DP metadata operations do not live on the connector itself: they are\n provided by the pluggable ``CAMP2pAFDControlPlane`` instance created at\n construction time and exposed as ``control_plane``. The connector still\n owns the ``p2p`` process group the control plane transmits over, because\n creating that group is part of the collective ``init_afd_connector``" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L554-L564", + "text": " )\n ubatch_idx = metadata.stage_idx\n forward_context = get_forward_context()\n forward_context.cam_afdtransfer_state = transfer_state\n forward_context.ubatch_idx = ubatch_idx\n\n torch.ops.vllm.afd_camp2p_send_attn_output(\n hidden_states,\n self.hccl_comm_name,\n self.hccl_comm_name2,\n self.hccl_comm_name3," + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L660-L663", + "text": " ffn_size=self.ffn_size,\n fallback=max_num_tokens,\n )\n metadata = AFDTransferMetadata.create_ffn_metadata(" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L725-L742", + "text": " **kwargs: Any,\n ) -> None:\n \"\"\"Send processed model data from an FFN rank back to Attention.\n\n Args:\n ffn_output: Model data produced by the FFN layers.\n context: Transfer context saved when FFN received the Attention\n output; its ``states`` carries the CAMP2p receive-time results.\n **kwargs: An optional ``ubatch_idx``. If omitted, the method uses\n the ubatch number stored in ``context.metadata``.\n\n Raises:\n RuntimeError: If communication is not ready, required receive\n information is missing, or the ubatch group does not exist.\n \"\"\"\n if not self._initialized:\n raise RuntimeError(\"CAMP2P connector is not initialized\")\n states = cast(CAMP2PTransferState, context.states)" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n" + }, + "id": "init-synthetic-shifted_range-deepen-AFD-I66", + "mutation": "shifted_range", + "note": "kb init synthetic bad case from good rule AFD-I66: evidence moved off the lines the rule describes", + "rule_id": "AFD-I66", + "source": "synthetic", + "stage": "deepen", + "synthetic": true, + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-skeleton-AFD-I14.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-skeleton-AFD-I14.json new file mode 100644 index 00000000..4ad7812c --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-skeleton-AFD-I14.json @@ -0,0 +1,39 @@ +{ + "base": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L131-L134", + "text": "| Adapter | Current purpose |\n| --- | --- |\n| [`model_executor/routing_simulator.py`](../../../afd_plugin/model_executor/routing_simulator.py) | Registers the opt-in `afd_balanced` strategy through vLLM's public `RoutingSimulator` interface. Select it with `VLLM_MOE_ROUTING_SIMULATION_STRATEGY=afd_balanced`; `AFD_BENCHMARK_FORCE_LB_TOPN_PER_RANK` optionally limits each rank's expert pool. It provides deterministic source-rank phases and normalized weights for controlled CUDA performance tests without replacing router internals. EPLB remains outside the validated scope. |\n| [`compat/npu/runtime_config.py`](../../../afd_plugin/compat/npu/runtime_config.py) | Mirrors vLLM-Ascend's non-SP all-to-all backend rewrite for custom AFD workers and reports the active NPU ubatch count. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L175-L191", + "text": "- Patch import errors are currently best effort at plugin startup and may be\n visible only in debug logs. Runtime paths must therefore fail explicitly if\n a required adaptation is absent rather than silently serving incorrect\n results.\n- Copied non-AFD branches are intended to preserve pinned upstream behavior;\n they still require regression tests because global replacement affects every\n process that loads the plugin.\n- A patch that saves an original callable must never overwrite that saved\n original with its own wrapper on reload.\n- Patch removal requires deleting registration/application code and focused\n tests only after the replacement upstream behavior is part of the supported\n pinned stack.\n\n## Candidate invariants\n\nThe following RFC candidates are non-normative while this document is draft:\n" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L187-L189", + "text": "\n## Candidate invariants\n" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L223-L224", + "text": "[#86](https://github.com/JiusiServe/afd-plugin/issues/86) may remove or relocate\npatches. The documentation RFC remains tracked by" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n" + }, + "id": "init-synthetic-shifted_range-skeleton-AFD-I14", + "mutation": "shifted_range", + "note": "kb init synthetic bad case from good rule AFD-I14: evidence moved off the lines the rule describes", + "rule_id": "AFD-I14", + "source": "synthetic", + "stage": "skeleton", + "synthetic": true, + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-skeleton-AFD-I18.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-skeleton-AFD-I18.json new file mode 100644 index 00000000..0cec6196 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-skeleton-AFD-I18.json @@ -0,0 +1,64 @@ +{ + "base": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L101-L102", + "text": "\n```bash" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L299-L307", + "text": "Ascend custom ops are built automatically only when the build environment looks\nlike an Ascend NPU platform, for example when `torch_npu`, CANN environment\nvariables, or the default Ascend toolkit path are present. GPU builds skip\nAscend ops by default. Set `AFD_BUILD_ASCEND_OPS=1` or\n`AFD_BUILD_ASCEND_OPS=0` to override the auto-detection.\n\n## E2E Test\n\nTo run E2E tests, use the [`run-e2e` skill](.agents/skills/run-e2e/SKILL.md)." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L100-L103", + "text": "| Worker/device lifecycle | vLLM `Worker` plus AFD role worker | vLLM-Ascend `NPUWorker` plus AFD role worker |\n| Attention execution | upstream V1 or V2 `GPUModelRunner` extension | upstream V1 or V2 `NPUModelRunner` extension |\n| FFN execution | plugin minimal runner | upstream `NPUModelRunner` extension |\n| Graph policy/keying | `v1/worker/cuda_graph.py` | shared policy/keying plus ACL/NPUGraph integration |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L150-L153", + "text": "- native ubatching with graphs is accepted only for exactly two ubatches;\n- other graph modes fail before runtime execution.\n\nAttention treats DP metadata transfer as a control-plane side effect. For a" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L314-L319", + "text": "See [the routed-only guide](../../npu/CAM_ASYNC_ROUTED_OPS.md) for metadata\nand window lifetime contracts. Device validation remains necessary for each\ntarget topology.\n\nThe routed-only operator host tiling reads `HCCL_BUFFSIZE` in MiB, sharing\nthe HCCL runtime's environment setting. When a domain overrides allocation" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L366-L370", + "text": "\nThe following RFC candidate is non-normative while this document is draft:\n\n- `PLAT-INV-001`: CUDA and Ascend AFD classes do not inherit from each other;\n platform extensions use matching upstream classes, while" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L74-L75", + "text": "resolve to an `AFDConnectorBase` subclass; duplicate registration is rejected\nunless `replace=True`, and unknown names fail before resource initialization." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L109-L116", + "text": "installed and later ones skipped. Registration logs these failures at debug\nlevel and continues to model registration.\n\n## Monkey-patch inventory\n\nEvery row describes current pinned behavior. `Guard` is the patch's own guard,\nnot the package dependency policy.\n" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L129-L129", + "text": "These modules adapt upstream behavior without replacing a global symbol:" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n" + }, + "id": "init-synthetic-shifted_range-skeleton-AFD-I18", + "mutation": "shifted_range", + "note": "kb init synthetic bad case from good rule AFD-I18: evidence moved off the lines the rule describes", + "rule_id": "AFD-I18", + "source": "synthetic", + "stage": "skeleton", + "synthetic": true, + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-skeleton-AFD-I4.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-skeleton-AFD-I4.json new file mode 100644 index 00000000..ca6b78bc --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-skeleton-AFD-I4.json @@ -0,0 +1,54 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L101-L102", + "text": "\n```bash" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L299-L307", + "text": "Ascend custom ops are built automatically only when the build environment looks\nlike an Ascend NPU platform, for example when `torch_npu`, CANN environment\nvariables, or the default Ascend toolkit path are present. GPU builds skip\nAscend ops by default. Set `AFD_BUILD_ASCEND_OPS=1` or\n`AFD_BUILD_ASCEND_OPS=0` to override the auto-detection.\n\n## E2E Test\n\nTo run E2E tests, use the [`run-e2e` skill](.agents/skills/run-e2e/SKILL.md)." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L100-L103", + "text": "| Worker/device lifecycle | vLLM `Worker` plus AFD role worker | vLLM-Ascend `NPUWorker` plus AFD role worker |\n| Attention execution | upstream V1 or V2 `GPUModelRunner` extension | upstream V1 or V2 `NPUModelRunner` extension |\n| FFN execution | plugin minimal runner | upstream `NPUModelRunner` extension |\n| Graph policy/keying | `v1/worker/cuda_graph.py` | shared policy/keying plus ACL/NPUGraph integration |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L150-L151", + "text": "- native ubatching with graphs is accepted only for exactly two ubatches;\n- other graph modes fail before runtime execution." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L314-L318", + "text": "See [the routed-only guide](../../npu/CAM_ASYNC_ROUTED_OPS.md) for metadata\nand window lifetime contracts. Device validation remains necessary for each\ntarget topology.\n\nThe routed-only operator host tiling reads `HCCL_BUFFSIZE` in MiB, sharing" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L74-L75", + "text": "resolve to an `AFDConnectorBase` subclass; duplicate registration is rejected\nunless `replace=True`, and unknown names fail before resource initialization." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L109-L116", + "text": "installed and later ones skipped. Registration logs these failures at debug\nlevel and continues to model registration.\n\n## Monkey-patch inventory\n\nEvery row describes current pinned behavior. `Guard` is the patch's own guard,\nnot the package dependency policy.\n" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n" + }, + "id": "init-synthetic-shifted_range-skeleton-AFD-I4", + "mutation": "shifted_range", + "note": "kb init synthetic bad case from good rule AFD-I4: evidence moved off the lines the rule describes", + "rule_id": "AFD-I4", + "source": "synthetic", + "stage": "skeleton", + "synthetic": true, + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-skeleton-AFD-I7.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-skeleton-AFD-I7.json new file mode 100644 index 00000000..715f62bc --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-shifted_range-skeleton-AFD-I7.json @@ -0,0 +1,59 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L47-L48", + "text": "| DeepSeekV2 / DeepSeekV3 / DeepSeekV3.2 | `DeepseekForCausalLM`, `DeepseekV2ForCausalLM`, `DeepseekV3ForCausalLM`, `DeepseekV32ForCausalLM` | `AFDDeepseekForCausalLM`, `AFDDeepseekV2ForCausalLM`, `AFDDeepseekV3ForCausalLM` | DeepSeekV3.2 uses `AFDDeepseekV3ForCausalLM`. Each AFD role constructs and loads only its role-required model components, while shared embedding, normalization, and output components remain available where required by the model lifecycle. |\n| Qwen3 MoE | `Qwen3MoeForCausalLM` | `AFDQwen3MoeForCausalLM` | CUDA with `compute_gate_on_attention=false`. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L54-L54", + "text": "" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L86-L93", + "text": "Requires Python **3.10-3.13** and [uv](https://docs.astral.sh/uv/).\n\n```bash\ngit clone https://github.com/vllm-project/afd-plugin.git\ncd afd-plugin\nuv sync --group dev\n```\n" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L108-L115", + "text": "| `P2pNcclAFDConnector` | CUDA | FFN ranks, then Attention ranks | `P2pNcclAFDControlPlane`; stage DP metadata over a separate NCCL group | `connector.control_plane is not None` |\n| `CAMP2pAFDConnector` | Ascend | FFN ranks, then Attention ranks | `CAMP2pAFDControlPlane`; stage DP metadata over Gloo plus HCCL data groups | `connector.control_plane is not None` |\n| `CAMAsyncAFDConnector` | Ascend | Attention ranks, then FFN ranks | `None`; routing/token metadata travels with CAM dispatch payloads | `connector.control_plane is None` |\n\nThe CUDA P2P mapping requires\n`num_attention_ranks >= num_ffn_ranks`. Each FFN rank owns a subgroup containing\nitself and consecutive Attention peers; the peers are distributed in blocks that\ndiffer in size by at most one, so an integral A/F ratio is not required. CAMP2P also" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L159-L162", + "text": "Both roles validate the paired V2 topology, but only Attention uses the native\nV2 model runner. FFN remains connector-driven and uses its existing AFD runner\nsurface. CUDA V2 has DP2/TP2 eager and graph E2E evidence; the current Ascend\nV2 contract has focused unit evidence but no repository hardware E2E case." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L294-L303", + "text": "- does not synchronize stage eligibility across asynchronous DP replicas;\n- does not support prefill or decode context parallel metadata.\n\nThe Attention runner calls the device-independent planner in\n`model_executor/npu/async_cam_ubatching.py`; the NPU model path applies the plan\nthrough `models/npu/async_cam_layout.py`. FlashComm1/SP is Attention-local and\nFFN topology validation is independent. See the CAM async user guide for the\nsupported topology and environment matrix.\n\nCAM async requires eager execution and rejects vLLM native ubatching. Its" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L153-L159", + "text": "`connector.control_plane` interface:\n\n| Selection state | Connectors | Worker behavior |\n| --- | --- | --- |\n| `control_plane is not None` | `P2pNcclAFDConnector`, `CAMP2pAFDConnector` | Call `control_plane.recv_dp_metadata_list()`, then profile, warm, capture, replay, or execute its stage map. |\n| `control_plane is None` | `CAMAsyncAFDConnector` (NPU only) | Block directly on a connector work item; no separate DP-metadata control plane. |\n" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L350-L354", + "text": "[connector contracts](connector_contracts.md).\n\nThe repository does not record a canonical CUDA container or a released\nvLLM-Ascend v0.26 container. The NPU implementation records source commit\n`80d8c194f`; environment evidence is not an authoritative package tag." + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n" + }, + "id": "init-synthetic-shifted_range-skeleton-AFD-I7", + "mutation": "shifted_range", + "note": "kb init synthetic bad case from good rule AFD-I7: evidence moved off the lines the rule describes", + "rule_id": "AFD-I7", + "source": "synthetic", + "stage": "skeleton", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I15.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I15.json new file mode 100644 index 00000000..eaf2cd69 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I15.json @@ -0,0 +1,54 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L118-L121", + "text": "# Upstream backend initialization creates builder 0 for full-batch metadata.\n# Async CAM extends that list, and stage ``ubid`` uses builder\n# ``ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid``.\nASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET = 1" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L286-L306", + "text": " if self.afd_async_extra_info.async_moe_ubatching:\n self.ubatch_slices = None\n return self._build_attention_metadata_with_async_moe_ubatches(\n num_tokens=num_tokens,\n num_reqs=num_reqs,\n max_query_len=max_query_len,\n num_tokens_padded=num_tokens_padded,\n num_reqs_padded=num_reqs_padded,\n ubatch_slices=ubatch_slices,\n logits_indices=logits_indices,\n use_spec_decode=use_spec_decode,\n for_cudagraph_capture=for_cudagraph_capture,\n num_scheduled_tokens=num_scheduled_tokens,\n num_scheduled_tokens_np=num_scheduled_tokens_np,\n cascade_attn_prefix_lens=cascade_attn_prefix_lens,\n )\n self._afd_pending_metadata = self._build_afd_metadata(\n ubatch_slices,\n num_tokens,\n )\n self.ubatch_slices = ubatch_slices" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L368-L372", + "text": " self._afd_async_moe_ubatch_metadata = None\n self._afd_pending_metadata = self._build_afd_metadata(\n None,\n num_tokens,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L491-L494", + "text": " # ### PATCH START: Reserve Async CAM stage builders\n metadata_builder_offset = (\n ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET if is_async_moe_stage_build else 0\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L620-L625", + "text": " attn_group = self.attn_groups[kv_cache_gid][attn_gid]\n # ### PATCH START: Async CAM builder offset\n builder = attn_group.get_metadata_builder(\n metadata_builder_offset + (ubid or 0),\n )\n # ### PATCH END: Async CAM builder offset" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L670-L676", + "text": " # ### PATCH START: Isolate Async CAM builder inputs\n if is_async_moe_stage_build:\n isolate_deepseek_attention_builder_inputs(\n builder,\n common_attn_metadata,\n )\n # ### PATCH END: Isolate Async CAM builder inputs" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L216-L217", + "text": " if self.ubatch_slices is not None:\n forward_context.ubatch_slices = self.ubatch_slices" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-deepen-AFD-I15", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I15: evidence swapped for AFD-I43's (another module)", + "rule_id": "AFD-I15", + "source": "synthetic", + "stage": "deepen", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I21.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I21.json new file mode 100644 index 00000000..b9f836fd --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I21.json @@ -0,0 +1,59 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L126-L129", + "text": "`AFDConnectorBase` defines the lifecycle and data-plane shape below. Its\n`control_plane: AFDControlPlane | None` attribute is the explicit runtime\nselector: synchronous connectors install a control-plane object during\nconstruction, while CAM async leaves it as `None`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L275-L279", + "text": "The worker owns the connector object and calls `close()`. The connector owns\nthe groups/communicators it creates and the control-plane object it exposes;\nthe control plane does not have an independent lifetime. Model code may invoke\nconnector data-path methods through forward metadata but does not own those\nresources." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L289-L291", + "text": "- Callers must branch on `connector.control_plane is None` before using the\n control-plane interface. GPU runners require a non-`None` control plane;\n CAM async instead exposes connector-driven work-item methods." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L307-L309", + "text": "- `CAP-INV-001`: runtimes choose control-driven or connector-driven FFN steps\n from the explicit optional `connector.control_plane` interface, not a\n concrete class-name check." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L150-L164", + "text": "## Daemon step selection\n\nThe worker selects one of two FFN step paths from the optional\n`connector.control_plane` interface:\n\n| Selection state | Connectors | Worker behavior |\n| --- | --- | --- |\n| `control_plane is not None` | `P2pNcclAFDConnector`, `CAMP2pAFDConnector` | Call `control_plane.recv_dp_metadata_list()`, then profile, warm, capture, replay, or execute its stage map. |\n| `control_plane is None` | `CAMAsyncAFDConnector` (NPU only) | Block directly on a connector work item; no separate DP-metadata control plane. |\n\nThe connector-driven path exists only on Ascend. GPU FFN supports\ncontrol-plane-driven connectors exclusively: `GPUFFNModelRunner` asserts\n`connector.control_plane is not None` at construction, and the GPU daemon loop\nraises `NotImplementedError` if a connector without a control plane is ever\ninstalled." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L219-L220", + "text": "CAM async is eager-only and does not use the FFN graph-control path; the graph\ncache is keyed by DP metadata, which does not exist without a control plane." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L246-L265", + "text": "When `connector.control_plane` is not `None`, the runner sends an\n`AFDControlPayload` through that `AFDControlPlane` before model/data-plane\ntransfer. The payload contains a stage-indexed DP metadata map plus\n`is_warmup`, `is_graph_capturing`, and `is_profile` flags.\n\n- A non-ubatched request sends stage `0` and uses native or fallback DP\n metadata.\n- Native ubatching sends one DP metadata item per stage.\n- A padded full-graph request sends metadata for the padded capture token\n count.\n- An NPU V2 profile forward marks `is_profile` so FFN recreates the matching\n Ascend profile context and balanced dummy-MoE state.\n- The control plane updates its owning connector's local state before sending\n the payload.\n\n`P2pNcclAFDConnector` and `CAMP2pAFDConnector` install\n`P2pNcclAFDControlPlane` and `CAMP2pAFDControlPlane`, respectively.\n`CAMAsyncAFDConnector.control_plane` remains `None`, so the Attention runner\nskips DP metadata coordination; CAM dispatch payloads carry the routing and\ntoken metadata required to drive FFN work." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L287-L292", + "text": "The FFN runner owns a separate ACL graph cache keyed by stage token counts and\nA/F topology. Warmup runs the eager FFN path. Formal capture updates connector\nstate through `connector.control_plane` before entering `torch.npu.graph(...)`,\nso replay contains only model and data-plane operations. An unknown key falls\nback to eager execution. CAM async does not enter this path because validation\nrequires eager execution and `connector.control_plane` is `None`." + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-deepen-AFD-I21", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I21: evidence swapped for AFD-I2's (another module)", + "rule_id": "AFD-I21", + "source": "synthetic", + "stage": "deepen", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I24.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I24.json new file mode 100644 index 00000000..de90fcc8 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I24.json @@ -0,0 +1,69 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L145-L149", + "text": "| Surface | Caller and current responsibility |\n| --- | --- |\n| `update_state_from_dp_metadata(payload)` | Applies local stage shape/graph/warmup state to the owning connector without sending it. |\n| `send_dp_metadata_list(payload)` | Moves `AFDControlPayload` from Attention to FFN for control-plane-driven connectors. |\n| `recv_dp_metadata_list()` | Blocks on and returns the next control payload that drives an FFN step. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L163-L164", + "text": "| `AFDDPMetadata` | CPU token counts plus DP/SP-compatible sizing helpers. It adapts upstream-like metadata into a plugin-owned representation. | Wire representation is the candidate contract; helper surface is draft. |\n| `AFDControlPayload` | Stage-to-DP-metadata mapping plus graph-capture, warmup, and profile flags. | Candidate control envelope. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L171-L176", + "text": "The control-plane codec serializes only plugin-owned primitive data: integer\nstage keys, token-count lists, max counts, and graph/warmup/profile boolean\nflags. It encodes JSON,\nsends a size followed by a `uint8` tensor, and reconstructs\n`AFDControlPayload`; it does not serialize a vLLM `DPMetadata` object. Tensor\ndata-path layout remains connector-specific." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L301-L302", + "text": "- `XFER-INV-001`: control-plane wire data uses plugin-owned payloads rather\n than serialized vLLM-internal metadata objects." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L317-L320", + "text": "Changes must validate topology, process-group lifecycle, payload round trips,\nand connector-specific cleanup using the unit paths above. A payload change\nmust update all producers, consumers, codec tests, model handoffs, and both role\nruntimes. A process-group change requires review against the pinned PyTorch and" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L244-L262", + "text": "## Control-plane coordination\n\nWhen `connector.control_plane` is not `None`, the runner sends an\n`AFDControlPayload` through that `AFDControlPlane` before model/data-plane\ntransfer. The payload contains a stage-indexed DP metadata map plus\n`is_warmup`, `is_graph_capturing`, and `is_profile` flags.\n\n- A non-ubatched request sends stage `0` and uses native or fallback DP\n metadata.\n- Native ubatching sends one DP metadata item per stage.\n- A padded full-graph request sends metadata for the padded capture token\n count.\n- An NPU V2 profile forward marks `is_profile` so FFN recreates the matching\n Ascend profile context and balanced dummy-MoE state.\n- The control plane updates its owning connector's local state before sending\n the payload.\n\n`P2pNcclAFDConnector` and `CAMP2pAFDConnector` install\n`P2pNcclAFDControlPlane` and `CAMP2pAFDControlPlane`, respectively." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L315-L327", + "text": "During graph warmup/capture, the Attention runner marks control payloads so the\nFFN daemon can warm or capture the matching shape. A non-ubatched capture sends\nthe control payload explicitly before formal capture; an ubatched capture lets\nthe platform wrapper send the exact per-stage shape. CUDA Graph, ACL Graph,\nstream, and wrapper implementation details are owned by\n[execution platforms](execution_platforms.md).\n\nV2 rejects DBO and ubatching. For full-graph capture, the V2 runner wraps the\nnative capture input-preparation seam and publishes exactly one warmup and one\ncapture payload for every native descriptor before the graph body. Native\nfull-graph replay does not create a `ForwardContext`, so an execute-scoped\nmanager hook publishes the padded control shape immediately before replay.\nAll temporary hooks and pending metadata state are restored in `finally`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L153-L156", + "text": "Attention treats DP metadata transfer as a control-plane side effect. For a\nsingle-stage capture it sends the padded capture shape before entering formal\nCUDA Graph capture. For an ubatched capture, `AFDUBatchWrapper` supplies the\nexact stage slices and sends per-stage metadata before the graph body." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L166-L170", + "text": "FFN uses the Attention payload's warmup/capture flags. It creates a shared\nCUDA graph memory pool, uses `connector.control_plane` to update the owning\nconnector state before `torch.cuda.graph(...)`, captures only\nmodel/data-plane work, and stores the graph by `make_ffn_graph_key()`. A\nmatching future payload replays the graph; a missing key runs eagerly." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L287-L290", + "text": "The FFN runner owns a separate ACL graph cache keyed by stage token counts and\nA/F topology. Warmup runs the eager FFN path. Formal capture updates connector\nstate through `connector.control_plane` before entering `torch.npu.graph(...)`,\nso replay contains only model and data-plane operations. An unknown key falls" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-deepen-AFD-I24", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I24: evidence swapped for AFD-I10's (another module)", + "rule_id": "AFD-I24", + "source": "synthetic", + "stage": "deepen", + "synthetic": true, + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I25.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I25.json new file mode 100644 index 00000000..73a8f678 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I25.json @@ -0,0 +1,39 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L118-L136", + "text": " def _install_afd_ubatch_wrapper(self) -> None:\n if isinstance(self.model, AFDUBatchWrapper):\n self.model.configure_afd_context_provider(\n self.install_afd_metadata_on_forward_context,\n )\n return\n\n model = self.model\n if isinstance(model, UBatchWrapper):\n model = model.unwrap()\n self.model = AFDUBatchWrapper(\n model,\n self.vllm_config,\n CUDAGraphMode.NONE,\n self.device,\n )\n self.model.configure_afd_context_provider(\n self.install_afd_metadata_on_forward_context,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L588-L599", + "text": "@contextmanager\ndef _use_afd_ubatch_wrapper_during_load(enabled: bool):\n if not enabled:\n yield\n return\n\n original = gpu_model_runner.UBatchWrapper\n gpu_model_runner.UBatchWrapper = AFDUBatchWrapper\n try:\n yield\n finally:\n gpu_model_runner.UBatchWrapper = original" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_worker.py:L85-L97", + "text": " afd_model_config = get_afd_model_config(\n self.vllm_config.model_config,\n device_type=\"cuda\",\n )\n self.vllm_config.model_config = afd_model_config\n self.model_config = afd_model_config\n\n native_runner_cls = native_runner_module.GPUModelRunner\n native_runner_module.GPUModelRunner = afd_runner_cls\n try:\n super().init_device()\n finally:\n native_runner_module.GPUModelRunner = native_runner_cls" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L139-L143", + "text": " v2_cudagraph_utils.prepare_inputs_to_capture = prepare_inputs_to_capture\n try:\n yield\n finally:\n v2_cudagraph_utils.prepare_inputs_to_capture = original_prepare" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-deepen-AFD-I25", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I25: evidence swapped for AFD-I38's (another module)", + "rule_id": "AFD-I25", + "source": "synthetic", + "stage": "deepen", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I26.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I26.json new file mode 100644 index 00000000..00a6b78d --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I26.json @@ -0,0 +1,54 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L147-L148", + "text": " graph_key = make_ffn_graph_key(dp_metadata_list)\n cuda_graph_info = self._cuda_graphs.get(graph_key)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L306-L306", + "text": " graph_key = make_ffn_graph_key(dp_metadata_list)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L322-L326", + "text": " self._cuda_graphs[graph_key] = {\n \"graph\": cudagraph,\n \"input_hidden_states\": output,\n \"output\": output,\n }" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L223-L225", + "text": " graph_key = self._make_graph_key(dp_metadata_list)\n acl_graphs = self._acl_graphs\n graph_info = acl_graphs.get(graph_key)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L256-L265", + "text": " def _make_graph_key(\n self,\n dp_metadata_list: dict[int, DPMetadata | AFDDPMetadata],\n ) -> tuple:\n return make_ffn_graph_key(\n dp_metadata_list,\n attention_size=int(self.connector.attn_size),\n ffn_size=int(self.connector.ffn_size),\n fallback=int(self.max_num_tokens),\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L488-L494", + "text": " graph_key = self._make_graph_key(dp_metadata_list)\n if graph_key in self._acl_graphs:\n logger.debug(\n \"AFD NPU FFN ACL graph capture skipped for existing key=%s\",\n graph_key,\n )\n return" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L518-L521", + "text": " self._acl_graphs[graph_key] = {\n \"graph\": graph,\n \"output\": output,\n }" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-deepen-AFD-I26", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I26: evidence swapped for AFD-I45's (another module)", + "rule_id": "AFD-I26", + "source": "synthetic", + "stage": "deepen", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I29.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I29.json new file mode 100644 index 00000000..5f95f3d6 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I29.json @@ -0,0 +1,29 @@ +{ + "base": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_worker.py:L80-L111", + "text": " if self.use_v2_model_runner:\n # ### PATCH START: construct the FFN runner through the V2 seam.\n validate_gpu_model_runner_v2_config(\n self.vllm_config,\n expected_role=\"ffn\",\n device_type=self.device_config.device_type,\n )\n afd_model_config = get_afd_model_config(\n self.vllm_config.model_config,\n device_type=\"cuda\",\n )\n self.vllm_config.model_config = afd_model_config\n self.model_config = afd_model_config\n\n native_runner_cls = gpu_model_runner_v2.GPUModelRunner\n gpu_model_runner_v2.GPUModelRunner = GPUFFNModelRunner\n try:\n super().init_device()\n finally:\n gpu_model_runner_v2.GPUModelRunner = native_runner_cls\n torch.accelerator.empty_cache()\n return\n # ### PATCH END: construct the FFN runner through the V2 seam.\n\n fail_if_unsupported_ubatching(self.vllm_config)\n\n super().init_device()\n self.vllm_config.model_config = get_afd_model_config(\n self.vllm_config.model_config,\n device_type=\"cuda\",\n )\n self.model_runner = GPUFFNModelRunner(self.vllm_config, self.device)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L64-L64", + "text": " def __init__(self, vllm_config: VllmConfig, device: object) -> None:" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/model-integration/rules.md": "---\ntitle: \"AFD 模型包装(afd_plugin/model_executor/models)评审规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD 模型包装(afd_plugin/model_executor/models)评审规则\n\n## AFD-I15 — V2 `_checkpoint_weight_roles` 与 `AFDDeepseekV2DecoderLayer.__init__` 的分支必须一一对应\n\n- `_is_moe_layer` 复制了 `__init__` 里 `is_moe_layer` 的判定,依据是 `n_routed_experts`、`first_k_dense_replace` 和 `moe_layer_freq`。改了其中一处,另一处必须同步改。否则权重会被分到没有构造该模块的角色上,加载时要么静默丢失,要么报 unexpected key。\n - dense 层在 `compute_gate_on_attention` 时由 Attention 构造 `native.DeepseekV2MLP`,FFN 侧是 `PPMissingLayer`。\n - MoE 的 `mlp.gate` 在 `compute_gate_on_attention` 时两侧都持有,所以返回 `_BOTH_ROLES`。\n - `mlp.shared_experts` 只在 `AFD_ASYNC_CONNECTOR` 下归 Attention:由 `GateOnlyRemoteMoE` 构造,FFN 侧不构造。\n- `load_weights` 传入的 `attention_shared_experts=... == AFD_ASYNC_CONNECTOR` 必须和 `GateOnlyRemoteMoE.shared_experts` 的构造条件使用同一个判断。\n- 如果某个角色新增或删除子模块,或者改变了 `self_attn`/`mlp` 下的命名,必须在分类函数里补对应分支。\n- `_iter_role_weights` 必须是只消费一次的生成器。不要对 checkpoint 迭代器做 `list()`,也不要遍历两次。\n\n\n\n## AFD-I21 — 远端 FFN 代理必须按 send → DBO yield → recv 的顺序执行,并使用同一个 `stage_idx`\n\n- `RemoteFFNProxy._send_and_receive` 和 `RemoteDeepseekV4FFN.forward` 是同一个协议的两份实现,改一处必须同步另一处。步骤依次是:\n 1. 取 `forward_context.ubatch_idx` 作为 `stage_idx`,没有时回退到 `afd_metadata.stage_idx`,并把它写回 `afd_metadata.stage_idx`。\n 2. 调用 `send_attn_output`。\n 3. 调用 `maybe_apply_dbo_yield(..., role=\\\"attention\\\")`。\n 4. 调用 `recv_ffn_output(ref_tensor=..., ubatch_idx=stage_idx)`。\n- 如果把 yield 挪到 send 之前或 recv 之后,DBO 两个 ubatch 的通信和计算就不再重叠,收发还可能错配。\n- `recv_ffn_output` 的 `ref_tensor` 必须是 yield 返回的张量。`create_attention_metadata` 的 `seq_len` 必须等于发送张量第 0 维的长度。\n- 额外的 send kwargs 必须和 FFN 侧消费的内容一致:\n - V4 必须发送 1-D、与 token 对齐的 `input_ids`。这由 `AFDDeepseekV4ForCausalLM.afd_requires_input_ids = True` 声明,并由 `AFDDeepseekV4DecoderLayer.compute_ffn_output` 强制要求。\n - V2 的 `AFDAttentionFusedMoE` 明确拒绝 `input_ids`。\n\n\n\n## AFD-I22 — CUDA remote experts:是否发送 `router_logits`,必须与 FFN runner 的外部路由模式成对\n\n- 只有在 `is_internal_router=False`(即 `compute_gate_on_attention`)时,Attention 侧的 `AFDAttentionFusedMoE` 才发送 `router_logits`。\n- FFN 侧在同一条件下设置 `self.mlp.experts.gate = None`,让 runner 走外部路由;`compute_experts_output` 以 `is_internal_router` 作为守卫。两侧的条件必须一致。\n- `get_experts_routing_spec` 用 `layer.mlp.gate.out_dtype or gate.weight.dtype` 和 `layer.mlp.n_routed_experts` 生成图捕获用的静态 spec,因此:\n - `AFDDeepseekV2RemoteExpertsMoE` 必须保留 `n_routed_experts`。\n - `GateLinear(out_dtype=native._get_moe_router_dtype(config))` 必须与 native gate 一致。\n- Attention 侧在 `compute_gate_on_attention=False` 时 `gate` 为 `None`,这时调用 `get_experts_routing_spec` 会抛 `AttributeError`。新增调用点时必须确认对应组合下存在 gate。\n- 两个角色各自计算 `uses_remote_experts`(`device_type == \\\"cuda\\\" and is_moe_layer`),得到的层集合必须相同,不能让它依赖 role。\n- `AFDAttentionFusedMoE.update_expert_map` 是空实现,而 Attention 侧会在 `enable_eplb` 时拒绝启动。放开 EPLB 之前,必须先实现真实的 expert map。\n\n\n\n## AFD-I24 — FFN 侧构造 MoE:在私有配置副本上用 `None` 关闭 shared experts;在 NPU 上先刷新 `FusedMoE` 绑定再构造\n\n- 在 `AFD_ASYNC_CONNECTOR` 下,FFN 侧先 `copy(config)`,再对副本执行 `object.__setattr__(moe_config, \\\"n_shared_experts\\\", None)`。\n- 只能改副本:原始 `config` 还会被 `_checkpoint_weight_roles`、`GateOnlyRemoteMoE` 以及后续各层读取。\n- 哨兵值必须是 `None`:写 `0` 会构造出一个零宽 MLP。\n- 在 NPU 上,`native.FusedMoE = fused_moe.FusedMoE` 必须在构造 `native.DeepseekV2MoE` 之前执行:native 模块在 import 时就绑定了 `FusedMoE`,而 AFD 可能早于 vLLM-Ascend 打补丁就导入了它。不要把这条赋值移到模块顶层或构造之后。\n\n\n\n## AFD-I25 — 绕过 native `__init__` 的包装类,必须自行设置 native forward/load_weights 会读取的所有属性\n\n- 以下类都直接调用 `nn.Module.__init__`,但仍复用 native 的 `forward` 或 `load_weights`(例如 `super().forward`、`super().load_weights`):`AFDDeepseekV2RemoteExpertsMoE`、`AFDDeepseekV2DecoderLayer`、`AFDDeepseekV2Model`、`AFDDeepseekV4DecoderLayer`、`AFDDeepseekV4Model`。\n- 因此不要删除下列属性:`is_sequence_parallel`、`n_logical_experts`/`n_physical_experts`/`n_local_physical_experts`、`use_mha`、`num_redundant_experts`(代码注释写明 Needed by load_weights)、`aux_hidden_state_layers`、`make_empty_intermediate_tensors`、`routed_scaling_factor`、`use_sequence_parallel_moe`,以及 V4 的 `hc_head_*`、`_mtp_hidden_buffer`、`use_mega_moe`。缺少这些属性时,往往只在 EPLB、PP、fp16 这类特定路径上才会报错。\n- 每个 patch 头都记录了 vLLM v0.26.0 和 commit `568afb3a13806beb53bb2e6bd518269357b237c0`,`### PATCH START/END` 之外的代码应与该 commit 的 native 实现保持一致。升级 vLLM 时,必须重新 diff native `__init__`,补上新增的属性,并更新 patch 头。\n\n\n\n## AFD-I26 — `afd_plugin.model_executor.models.npu` 只能在函数内部延迟导入\n\n- `deepseek_v2.py` 在 CUDA 和 NPU 上都会被导入,而 `npu` 子模块在顶层导入了 `vllm_ascend`(例如 `deepseek_attention_metadata.py`)。\n- 目前所有 npu helper 都是在函数内部导入的:`GateOnlyRemoteMoE.forward`、`compute_attn_output`、`compute_ffn_output`、`AFDDeepseekV2Model.forward`。\n - 在 CUDA 或 CPU 环境下导入会失败。\n - `deepseek_v2_async_cam_forward.py` 只在 `TYPE_CHECKING` 下反向导入 `deepseek_v2`,提到顶层会形成循环导入。\n- 新增的 NPU 专属路径也必须采用延迟导入,并在调用处用 `device_type` 或 connector 做判断。\n\n\n\n## AFD-I28 — `prepare_cam_dispatch_payload` 与 `restore_cam_dispatch_output` 必须成对使用同一个 `CAMDispatchLayout`,并且所有 TP rank 都要执行\n\n- 在非 SP 且 `tp_size > 1` 时:\n - dispatch 按 ceil-div 把复制的 token 维分片,尾部补零。\n - restore 调用 `tensor_model_parallel_all_gather` 拼回完整张量,再截到 `parent_tokens`。\n - restore 必须使用 prepare 返回的 `layout`,不能重新计算。\n- restore 里的 all-gather 是 TP 集合通信。即使本 rank 的分片全是 padding 也必须调用 restore;任何按 rank 或本地 token 数跳过调用的分支都会导致死锁。\n- CAM 输出的第 0 维必须等于 `layout.local_tokens`(包含 padding 行),dispatch 和 combine 之间不要去掉 padding。\n- `topk_weights`、`topk_ids`、`router_logits` 必须与 `hidden_states` 共用 token 维,并按同样的方式切分。新增的 per-token 张量也要经过 `_pad_first_dim(...)[local_token_slice]`。\n\n\n\n## AFD-I30 — Async CAM 下多份 attention metadata 同时存活:stage build 之前先隔离输入,每次 build 之后立即物化\n\n- full-batch、stage-0、stage-1 三份 metadata 都在执行之前构建。vLLM-Ascend builder 返回的 `cos`/`sin` 可能是进程级 RoPE buffer 的视图,后一次 build 会覆盖前一个对象的内容。\n- 每次 build 之后、下一次 build 之前,必须调用 `materialize_deepseek_attention_metadata_by_layer`。\n- 每个 stage builder 调用之前,必须先对该 stage 的 common metadata 调用 `isolate_deepseek_attention_builder_inputs`:\n - SFA 会原地写 `group_len`/`group_key_idx`/`group_key_cache_idx`。\n - DSA 会原地清零 `block_table_tensor` 的 padding 行。\n- 两个函数都靠 `isinstance` 分派,遇到不认识的类型会静默跳过。新增或升级 backend 时必须补上对应分支。\n- DSA 用 `input_positions[: metadata.num_input_tokens]` 重建 RoPE,传入的必须是该 metadata 所属批次的 positions。\n\n\n\n## AFD-I29 — `AsyncMoeUbatchMetadata` 的 stage 规划不变量是布局还原正确的前提\n\n- 生成 metadata 的一方必须保证:\n - `attn_metadata` 与 `stages` 长度相同且非空。\n - `token_slice` 从 0 开始,连续、有序、非空。\n - `actual_tokens <= input_tokens`。\n - `parent_input_tokens` 覆盖全部真实 token。\n- 还原时直接对各 stage 裁剪后的输出做 `torch.cat`,再补齐到 `parent_input_tokens`,所以依赖上面的连续性。\n- 在 SP 下,要求 `tp_size > 1`,并且 `parent_input_tokens` 和每个 `stage.input_tokens` 都能被 `tp_size` 整除。\n- 每个 stage 的输出行数必须正好等于其物理 `input_tokens`:SP 下看 all-gather 之后的行数。\n- 还原时只保留前 `actual_tokens` 行,因此真实 token 必须排在 stage 的最前面。\n- positions 的 token 维探测优先 axis 1(对应 `[axes, N]` 形状),`llama_4_scaling` 沿用 positions 的维度。不要改成优先 axis 0,否则遇到方阵形状时会把位置轴误当成 token 轴。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-deepen-AFD-I29", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I29: evidence swapped for AFD-I54's (another module)", + "rule_id": "AFD-I29", + "source": "synthetic", + "stage": "deepen", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I34.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I34.json new file mode 100644 index 00000000..fef554cf --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I34.json @@ -0,0 +1,44 @@ +{ + "base": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L253-L272", + "text": " num_tokens = int(input_ids.numel())\n if num_tokens != expected_tokens:\n raise ValueError(\n f\"input_ids token count {num_tokens} does not match the AFD \"\n f\"transfer token count {expected_tokens}\",\n )\n\n ids = (\n input_ids.reshape(-1)\n .to(dtype=torch.int32)\n .unsqueeze(1)\n .expand(-1, topk)\n .contiguous()\n )\n scales = torch.zeros(\n (num_tokens, topk),\n dtype=torch.float32,\n device=input_ids.device,\n )\n return ids, scales" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L300-L308", + "text": " received_tokens = int(sim_expert_ids.shape[0])\n if received_tokens < expected_tokens:\n raise ValueError(\n f\"received {received_tokens} token ids but the FFN rank computes on \"\n f\"{expected_tokens} tokens; the ids channel is not aligned with the \"\n \"FFN token layout\",\n )\n # The columns are replicas of the same id, so the first one carries it.\n return sim_expert_ids[:expected_tokens, 0].contiguous()" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L538-L548", + "text": " input_ids = cast(torch.Tensor | None, kwargs.get(\"input_ids\"))\n expert_ids: torch.Tensor | None = None\n expert_scales: torch.Tensor | None = None\n compute_gate = 0\n if input_ids is not None:\n expert_ids, expert_scales = prepare_token_id_transfer(\n input_ids,\n topk=self.num_experts_per_tok,\n expected_tokens=metadata.total_tokens,\n )\n compute_gate = 1" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L651-L654", + "text": " layer_idx: int = kwargs.get(\"layer_idx\", 0)\n max_num_tokens: int = kwargs.get(\"max_num_tokens\", 0)\n recv_input_ids: bool = bool(kwargs.get(\"recv_input_ids\", False))\n compute_gate_mode = 1 if recv_input_ids else 0" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L702-L719", + "text": " # The ids slot is only written in the operator's ids mode, so the mode has\n # to match the sending rank's ``compute_gate``. It is declared here by the\n # receiving FFN rank through ``recv_input_ids`` rather than inferred from\n # the returned tensor: a genuine single-token layer would otherwise be\n # indistinguishable from the operator's placeholder. Reading the slot in\n # the other mode would hand the model uninitialised device memory as token\n # ids, which a token-keyed router turns into an out-of-range table read.\n received_ids: torch.Tensor | None = None\n if compute_gate_mode == 1:\n received_ids = received_token_ids(\n outputs[1],\n expected_tokens=batch_size,\n )\n return AFDA2FTransferPayload(\n hidden_states=outputs[0],\n context=context,\n input_ids=received_ids,\n )" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/attention-runtime/rules.md": "---\ntitle: \"afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# afd_plugin/v1/worker 审查规则:AFD 事务元数据、控制面发送与 runner 生命周期\n\n## AFD-I36 — V1/V2/NPU runner 必须初始化同一组 AFD 字段;NPU 直接依赖 `attention_metadata.py` 的私有 helper\n\n- `AFDMetadataProviderMixin` 只声明字段,由子类 `__init__` 负责初始化。三个 runner 都各自设置 `_is_warmup`、`_afd_is_graph_capturing`、`_afd_is_graph_replaying`、`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_afd_transaction_counter = 0`,并要求 connector 带 `control_plane`。新增字段或新 runner 时,三处要一起改。\n- `AFDNPUAttentionModelRunner` 不继承 mixin,而是直接 import `_forward_context_num_tokens`、`_full_cudagraph_padded_tokens`、`_resolve_world_ranks`、`build_ubatch_dp_metadata_list`。改这些下划线函数的签名或语义,等于改 NPU 的接口,需要同步 `afd_plugin/v1/worker/npu/attention_model_runner.py`。\n- 每次调用 `build_afd_metadata` 都会通过 `_next_afd_transaction_id` 消耗一个事务号。不要在只需读取或检查 metadata 的地方调用它。\n\n\n\n## AFD-I37 — connector 生命周期:rendezvous 放在 `load_model` 最后,`close()` 放在 `finally`\n\n- `init_afd_connector()` 是 Attention 和 FFN 之间的阻塞集合操作,同时充当“双方都加载完权重”的屏障。它必须在 `super().load_model()` 和 V1 的 `_install_afd_ubatch_wrapper()` 之后、内存 profiling 和首次前向之前执行,并保留 `is_initialized` 守卫(FFN 侧用同一个守卫)。不要把它挪回 `__init__` 或 wrapper 安装之前。\n- `shutdown` 里的 `self.connector.close()` 必须放在 `finally` 中,这样 native `super().shutdown()` 抛异常时仍会关闭。V2 还用嵌套 `finally` 保证 profiler 停止失败时不跳过后续清理。\n- V2 `__init__` 在 `super().__init__` 之后出错时,会关闭已创建的 connector、调用 `super().shutdown()`,然后重新抛出。新增的初始化步骤要放进这个 `try` 里。\n\n\n\n## AFD-I38 — 模块级符号替换的作用域要尽量窄,并在 `finally` 中恢复\n\n- 目前有三处替换,各自只在一个窗口内生效:`_use_afd_ubatch_wrapper_during_load` 只在 `super().load_model` 期间把 `gpu_model_runner.UBatchWrapper` 换成 `AFDUBatchWrapper`;`AFDAttentionWorker.init_device` 只在 `super().init_device()` 期间替换 `native_runner_module.GPUModelRunner`;V2 只在 `capture_model` 内替换 `v2_cudagraph_utils.prepare_inputs_to_capture`。这些替换依赖上游在调用时按模块属性查找符号。改动时要保留 try/finally 恢复,不要扩大替换窗口。\n- `init_device` 必须在调用 `super().init_device()` 之前,用 `get_afd_model_config(...)` 替换 `vllm_config.model_config` 和 `self.model_config`。否则 runner 会用非 AFD 的模型身份构造。\n- `_install_afd_ubatch_wrapper` 有两个分支:模型已经是 `AFDUBatchWrapper`,或需要先 unwrap 原生 `UBatchWrapper`。两个分支都必须调用 `configure_afd_context_provider(self.install_afd_metadata_on_forward_context)`。新增分支漏掉这一步,ubatch 前向就拿不到 AFD metadata。\n\n\n\n## AFD-I39 — `_determine_batch_execution_and_padding` 的 `args` 元组必须和 `_batch_execution_values` 的名称表逐位对齐\n\n- `args` 按位置构造,再由 `_batch_execution_values` 用 `zip(..., strict=False)` 映射成名字。如果上游签名插入或调换了参数,或者这里元组的顺序变了,映射会静默错位(例如把 `num_reqs` 当成 `num_tokens`),不会报错。改其中一处时,要同时改另一处,并对照上游签名核对。\n- DP=1 时,`_should_ubatch_single_rank` 必须保留三道守卫:`num_tokens < num_ubatches`、`check_ubatch_thresholds`,以及基于 padded `batch_descriptor.num_tokens` 的 `is_last_ubatch_empty`。DP>1 分支要保留“第一个 ubatch 不能为空”的检查。\n- ubatch 只支持 `num_ubatches == 2`,runner 和 worker 都通过 `fail_if_unsupported_ubatching` 检查。\n- `_afd_is_graph_replaying` 在这里由 `_is_warmup`、`_afd_is_graph_capturing` 和 `CUDAGraphMode.FULL` 算出,所以调用前这两个标志必须已经设置正确。\n\n\n\n## AFD-I40 — V1 `_build_attention_metadata`:先 stage AFD 事务,再临时关闭 `supports_update_block_table`\n\n- `_afd_pending_metadata` 必须在调用 `super()._build_attention_metadata` 之前,用 `ubatch_slices` 和未 padding 的 `num_tokens` 构建好。这样 `_model_forward` 里的 install 会复用这个事务,而不是按 context 的 token 数另建一个。\n- 有多个 ubatch 时,要遍历每个 ubatch 的 builder 并关闭 `supports_update_block_table`。只记录原本为 True 的 builder(按 `id` 去重),并在 `finally` 中恢复。漏掉恢复会让之后所有步骤都失去 block-table 快速更新。\n- 这是针对 vLLM PR #48659 的临时绕过。升级 pinned vLLM 时应评估能否删除,不要扩大它的作用范围。\n\n\n\n## AFD-I42 — V2 FULL replay hook 只作用于单个 manager 实例、不可重入,且只能在 `_use_afd_execution_context` 内使用\n\n- hook 只在 `cudagraph_mode == CUDAGraphMode.FULL_DECODE_ONLY` 且 `cudagraph_manager` 存在时启用。它用 `MethodType` 覆盖单个 manager 实例的 `run_fullgraph`,用 `_AFD_FULLGRAPH_HOOK_MARKER` 防止嵌套。退出时恢复之前的实例覆盖,或者 `del` 掉实例属性。不要改成修改类属性。\n- replay 的 metadata 先用真实的 `real_tokens`(`scheduler_output.total_num_scheduled_tokens`)构建,再把 `tokens_lens` 改成 padded 的 `desc.num_tokens`;DP 控制同样用 padded 数。`tokens_unpadded_lens` 保持真实值,两者不要合并成一个。\n- hook 自己只恢复 `_afd_is_graph_replaying`。`_afd_pending_metadata`、`_afd_suppress_metadata_send`、`_is_warmup`、`_afd_is_graph_capturing` 都靠外层 `_use_afd_execution_context` 的 `finally` 恢复,所以不能在这个作用域之外单独使用 hook。\n\n\n\n## AFD-I43 — NPU Async CAM stage 构建必须把 builder 0 留给全批次,并保持单 stage 的 AFD 事务\n\n- 全批次 metadata 已由 builder 0 构建,并在 stage 执行期间一直存活。因此 Async CAM stage 构建时用 `ASYNC_MOE_STAGE_METADATA_BUILDER_OFFSET + ubid` 选 builder。native DBO(`is_async_moe_stage_build=False`)的 offset 保持为 0。改动 offset 或 builder 的选择方式,会让 stage metadata 覆盖全批次 metadata 正在使用的可复用缓冲。\n- `isolate_deepseek_attention_builder_inputs` 只对 async stage 构建调用,而且不能省略,因为多个 stage 的 metadata 会同时存活。\n- async 路径必须置 `self.ubatch_slices = None`,并以单 stage(`build` 时传 `None`)构建 `_afd_pending_metadata`。否则 `_model_forward` 会把 stage 切片写进 `forward_context.ubatch_slices`,走上 DBO 路径。\n\n\n\n## AFD-I34 — `install_afd_metadata_on_forward_context` 只在 pending 为空时新建事务,顺序和子 context 判定不能改\n\n- 方法内顺序固定为:ubatch 子 context 直接返回(不发送)→ `_afd_pending_metadata is None` 时才 `build_afd_metadata` → 写入 `additional_kwargs[\"afd_metadata\"]` → 检查 `_afd_suppress_metadata_send` → 发送。调换顺序后,子 context 会重复发送,或者在 suppress 作用域里照样发送。\n- 非 None 的 pending 会被原样复用,包括它的 `transaction_id` 和 `tokens_lens`。现有入口各自保证 pending 是新的:V1 靠 `_build_attention_metadata` 每步重新 stage;capture 的 ubatch 分支显式置 None;V2 靠 `_use_afd_execution_context` 在退出时恢复原值。新增前向入口要确认不会复用上一步留下的旧 metadata。\n- `_is_ubatch_child_afd_context` 的判定条件是:`ubatch_slices is None`、`num_stages > 1`、`len(tokens_lens) == 1`。它依赖两件事:父 metadata 满足 `len(tokens_lens) == num_stages`(`build_afd_metadata` 多 stage 分支),子 metadata 只有一个 `tokens_lens` 元素且保留父 `num_stages`。改 metadata 字段布局或 wrapper 构造子 metadata 的方式时,要同步检查这个判定。\n- FULL graph 且没有 `ubatch_slices` 时,DP 控制用 `_full_cudagraph_padded_tokens` 给出的 padded 数,不能换成真实 token 数。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-deepen-AFD-I34", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I34: evidence swapped for AFD-I66's (another module)", + "rule_id": "AFD-I34", + "source": "synthetic", + "stage": "deepen", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I50.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I50.json new file mode 100644 index 00000000..4cbd0679 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I50.json @@ -0,0 +1,54 @@ +{ + "base": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L55-L57", + "text": "Compatibility code consumes the common plugin boundary and adapts pinned\nupstream behavior for AFD roles. Role modules may rely on those adaptations;\npatch modules must not become a general home for AFD-owned functionality." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L82-L82", + "text": "| CUDA DBO Attention metadata workaround | [`attention_model_runner.py`](../../../afd_plugin/v1/worker/attention_model_runner.py) | restoration and error-path coverage in [`test_attention_model_runner.py`](../../../tests/unit/v1/worker/test_attention_model_runner.py) |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L138-L149", + "text": "| [`v1/worker/attention_model_runner.py`](../../../afd_plugin/v1/worker/attention_model_runner.py) | Disables vLLM 0.26's block-table-only Attention metadata cache only while multiple DBO ubatches are built, then restores every affected builder in `finally`. Remove this workaround when [vLLM #48659](https://github.com/vllm-project/vllm/pull/48659), or an equivalent ubatch-aware cache key, reaches the pinned release. |\n\nSeveral scoped upstream adaptations live with their semantic owners rather\nthan this patch directory. Model dummy runs and V2 execution temporarily wrap\n`create_forward_context`; CUDA worker startup temporarily substitutes the V1\nor V2 runner symbol; V2 graph capture temporarily wraps native input\npreparation; and full-graph replay installs an instance-local manager hook.\nEvery scope restores symbols and runner state in `finally`. Connector process\ngroups also call private PyTorch/vLLM helpers as described in\n[connector contracts](connector_contracts.md). All of these seams must be\nincluded in an upstream upgrade audit even though they are not process-global\nimport-time patches." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L182-L183", + "text": "- A patch that saves an original callable must never overwrite that saved\n original with its own wrapper on reload." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L322-L327", + "text": "V2 rejects DBO and ubatching. For full-graph capture, the V2 runner wraps the\nnative capture input-preparation seam and publishes exactly one warmup and one\ncapture payload for every native descriptor before the graph body. Native\nfull-graph replay does not create a `ForwardContext`, so an execute-scoped\nmanager hook publishes the padded control shape immediately before replay.\nAll temporary hooks and pending metadata state are restored in `finally`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L132-L137", + "text": "Both CUDA workers delegate device and distributed setup to native\n`Worker.init_device()`. During that synchronous construction window, they\ntemporarily replace the selected upstream runner symbol with the AFD runner\nand restore it in `finally`. V1 Attention selects `AFDAttentionModelRunner`;\nV2 selects `AFDAttentionModelRunnerV2`; a V2-paired FFN still selects the\nminimal `GPUFFNModelRunner`. `torch.accelerator.empty_cache()` releases" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L158-L164", + "text": "ModelRunnerV2 rejects ubatching and reuses the native full-graph manager. Its\nAttention wrapper observes each native capture descriptor at the exact input\npreparation seam, publishes a warmup and capture payload outside the graph,\nand suppresses duplicate context sends. Full-graph replay bypasses context\ncreation, so an instance-scoped `run_fullgraph` hook publishes the padded\nshape immediately before replay. The wrapper verifies descriptor order and\ncall count and restores all temporary symbols and state in `finally`." + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/ffn-runtime/rules.md": "---\ntitle: \"FFN 角色运行时(worker / model runner)审查规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# FFN 角色运行时(worker / model runner)审查规则\n\n## AFD-I45 — graph 缓存的写入 key 与查询 key 必须由同一函数、同一参数计算\n\n- GPU 写入 `_cuda_graphs` 和查询它时都用 `make_ffn_graph_key(dp_metadata_list)`。NPU 统一走 `self._make_graph_key`,额外参数为 `attention_size`、`ffn_size` 和 `fallback=self.max_num_tokens`。\n- 只改一处(例如 NPU 某处直接调用 `make_ffn_graph_key(dp_metadata_list)`)会让 replay 一直 miss,静默退回 eager,或者反复 capture。\n- NPU 发现 key 已存在时跳过 capture,GPU 则直接覆盖。改 key 粒度时要同时评估这两种行为。\n\n\n\n## AFD-I46 — REPLAY 分支不执行 `_ffn_forward` 的 Python 代码,host 侧逻辑只在 capture 时运行一次\n\n- `graph_run_mode` 返回 `AFDGraphRunMode.REPLAY` 时,`execute_model` 只调用缓存 graph 的 `replay()` 就返回。此时不会执行 `update_state_from_dp_metadata`,`_ffn_forward` 里的元数据赋值、`_set_moe_layer_index`、日志和计数也都不会执行。\n- 在 `_ffn_forward` 中新增需要每步生效的 host 侧逻辑,会在 replay 时被静默跳过。这类逻辑必须放在 replay 早返回之前,或者本身就是能被录入 graph 的设备操作。\n- capture 时的 recv、compute、send 必须全部可录制。缓存条目中的 `\"output\"` 张量要保留引用,删掉它可能导致 graph 使用的内存被释放。\n\n\n\n## AFD-I47 — NPU `execute_ffn_step` 与 `execute_model` 互相调用,终止性依赖两处条件保持一致\n\n- `execute_ffn_step` 在 `use_aclgraph and (is_graph_capturing or is_warmup)` 时进入 `capture_model`。否则调用 `execute_model`,且不转发 `is_warmup` / `is_graph_capturing`。\n- `execute_model` 传给 `graph_run_mode` 之前,用 `graph_enabled`(即 `use_aclgraph`)屏蔽这两个标志。只有得到 WARMUP/CAPTURE 时才回调 `execute_ffn_step`,而同一条件下 `execute_ffn_step` 一定会进入 `capture_model`。\n- 以下改动都要重新确认不会无限互调,也不会把 capture 请求当作 eager 执行:开始透传这两个标志、去掉 `and graph_enabled` 屏蔽、让两处使用不同的开关。\n\n\n\n## AFD-I48 — FFN 侧的层集合和 (layer, stage) 收发顺序必须与 Attention 侧逐层发送一致\n\n- 循环结构固定为外层按层、内层按排序后的 `stage_ids`。每个 (layer, stage) 恰好一次 `recv_attn_output` 和一次 `send_ffn_output`。改顺序或次数会让 connector 阻塞或错配层。\n- `compute_gate_on_attention` 时,层集合在 GPU 上来自 `self.model.get_experts_layer_indices()`,在 NPU 上来自 `_ffn_layer_indices` → `_is_moe_layer`(依据 `n_routed_experts`、`first_k_dense_replace`、`moe_layer_freq`)。否则层集合为 `range(num_layers)`。修改这些判定时,必须与 Attention 侧的远端 MoE 层集合同步。\n- NPU layered W4A8 路径要求 `get_async_cam_w4a8_layers()` 返回的 `layer_idx` 与 `_ffn_layer_indices(self)` 完全相等。新增模型时不能绕过这项校验。\n\n\n\n## AFD-I51 — DP 与 AFD rank 之间的计数展开和投影依赖 TP rank 连续编号,长度不整除时会静默透传\n\n- `project_ffn_token_counts_to_dp` 和 NPU `_to_dp_level_token_counts` 都假设 FFN 计数排布为 `[dp0, dp0, dp1, dp1, ...]`,即同一 DP 下的 TP rank 连续且计数相同,并取每组的第一个。反向展开时(`aggregate_ffn_token_counts` 的 `expanded_counts`、NPU 的 `tp_size` 展开),同样假设 Attention rank 按 DP 连续编号。\n- rank 编号若改为交错排布,这些函数不会报错,但会静默取到错误的计数。\n- 长度不能被 `dp_size` 整除时,两个投影函数原样返回。下游 `AFDDPMetadata` 和 `ascend_forward_context(num_tokens_across_dp=...)` 会拿到长度不等于 `dp_size` 的计数,而 vLLM 的 `DPMetadata` 要求恰好 `dp_size` 项。新增拓扑时要确认满足整除。\n\n\n\n## AFD-I54 — GPU V2 runner 注入点:临时替换模块符号必须在 `finally` 中恢复,model config 要在构造 runner 前写回\n\n- V2 路径把模块级 `gpu_model_runner_v2.GPUModelRunner` 临时替换为 `GPUFFNModelRunner`,让 `super().init_device()` 构造出 FFN runner。必须用 `try/finally` 恢复原符号,否则同一进程里后续构造的 native runner 会变成 FFN runner。\n- V2 路径中,`get_afd_model_config` 的结果必须在 `super().init_device()` 之前写回 `self.vllm_config.model_config` 和 `self.model_config`,因为 runner 在 `super().init_device()` 内部构造。V1 路径则在其之后替换 config,再显式构造 runner。调整顺序时两条路径要分别检查。\n- `GPUFFNModelRunner.__init__(vllm_config, device)` 的签名要与 vLLM V2 `Worker.init_device` 构造 `GPUModelRunner` 的调用方式保持兼容。\n\n\n\n## AFD-I55 — 按 `connector.control_plane is None` 分流:GPU 只支持控制面驱动,NPU 两种模式的入口互斥\n\n- GPU:`GPUFFNModelRunner.__init__` 断言 `control_plane is not None`,`AFDFFNWorker._run_ffn_server_loop` 遇到 `None` 抛 `NotImplementedError`。`_ffn_forward`、`_dummy_run`、`capture_model` 都直接解引用 `self.connector.control_plane`。\n- NPU:`control_plane is None` 时只能走 `execute_connector_driven_step`。它反向要求 control_plane 为 None,并把 connector cast 为 `CAMAsyncAFDConnector`。`execute_ffn_step`、`_ffn_forward(update_connector_state=True)` 和 `_capture_graphs` 都要求有控制面。\n- 新增调用点必须按同一判定分流:connector-driven 路径不能调用 `update_state_from_dp_metadata` 或 graph capture,控制面路径也不能调用 `recv_ffn_work_item`。\n\n\n\n## AFD-I50 — `aggregate_ffn_token_counts` 的分块须与 `build_rank_mapping` 一致,每个 peer 先取 `max(1, ...)` 再求和\n\n- 每个 FFN rank 的 Attention 块必须通过 `afd_plugin.distributed::subgroup_attention_block` 获取。分块规则是连续、大小相差至多 1,并支持 3A2F 这类不能整除的情况。不要内联改成 `attention_size // ffn_size` 等分切片。\n- 零 token 的 peer 也贡献一行 placeholder,所以要对每个 peer 先 `max(1, count)` 再求和,缺失的 peer 用 `fallback`。把 `max(1, ...)` 挪到求和之后,4A2F 的 `(0, 4, 5, 6)` 会从 `(5, 11)` 变成 `(4, 11)`,与实际收到的行数不符。\n- NPU 的 `_ffn_token_counts_across_ranks` 是独立实现:要求整除,先求和再 `max(1, ...)`,fallback 为 `max_num_tokens`。改任一侧的计数语义时,要对照该平台 connector 实际下发的行数确认,不能默认两者共享语义。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-deepen-AFD-I50", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I50: evidence swapped for AFD-I6's (another module)", + "rule_id": "AFD-I50", + "source": "synthetic", + "stage": "deepen", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I62.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I62.json new file mode 100644 index 00000000..68b4376f --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-deepen-AFD-I62.json @@ -0,0 +1,49 @@ +{ + "base": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_metadata.py:L31-L36", + "text": " connector: AFDConnectorBase\n vllm_config: VllmConfig\n _is_warmup: bool\n _afd_transaction_counter: int\n _afd_is_profile: bool = False\n _afd_pending_metadata: AFDForwardContextMetadata | None" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_metadata.py:L226-L229", + "text": " def _next_afd_transaction_id(self) -> str:\n counter = self._afd_transaction_counter\n self._afd_transaction_counter = counter + 1\n return f\"afd-{counter}\"" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L82-L90", + "text": " assert self.connector.control_plane is not None, (\n \"GPU model runner only supports control-plane-driven connectors\"\n )\n self._is_warmup = False\n self._afd_is_graph_capturing = False\n self._afd_is_graph_replaying = False\n self._afd_pending_metadata: AFDForwardContextMetadata | None = None\n self._afd_suppress_metadata_send = False\n self._afd_transaction_counter = 0" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L295-L304", + "text": " if connector.control_plane is None:\n raise RuntimeError(\n \"AFD ModelRunnerV2 requires a control-plane-driven connector\",\n )\n self._is_warmup = False\n self._afd_is_graph_capturing = False\n self._afd_is_graph_replaying = False\n self._afd_pending_metadata: AFDForwardContextMetadata | None = None\n self._afd_suppress_metadata_send = False\n self._afd_transaction_counter = 0" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L102-L107", + "text": "from afd_plugin.v1.worker.attention_metadata import (\n _forward_context_num_tokens,\n _full_cudagraph_padded_tokens,\n _resolve_world_ranks,\n build_ubatch_dp_metadata_list,\n)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L159-L166", + "text": " self._is_warmup = False\n self._afd_is_graph_capturing = False\n self._afd_is_graph_replaying = False\n self._afd_pending_metadata: AFDForwardContextMetadata | None = None\n self._afd_suppress_metadata_send = False\n self._afd_transaction_counter = 0\n self._afd_async_moe_ubatch_metadata: AsyncMoeUbatchMetadata | None = None\n self._afd_live_execution = False" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/connectors/rules.md": "---\ntitle: \"NPU connector(CAM async / CAMP2P)review 规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# NPU connector(CAM async / CAMP2P)review 规则\n\n## AFD-I58 — async CAM FFN 侧:`token_nums_rankid_layeridx` 必须原样回传,真实层号和 token 数只来自 CAM 元数据\n\n- `afd_async_dispatch_recv` 返回的 `batch_info` 存为 `states.token_nums_rankid_layeridx`,`send_ffn_output` 必须把它原样传给 `afd_async_combine_send`。\n- 其中 `[0]` 是整个 rank 的总 token 数,而本次 chunk 的 token 数来自 `group_list.sum()`。不能为了对齐 `num_tokens` 去切片或重建这个张量。\n- `recv_ffn_work_item` 依赖的布局是:`[0]` 为总 token 数,`[2]` 为 layer_idx。算子布局变化时要同步修改这里。\n- FFN 侧调用 `recv_attn_output` 时传的是 `layer_idx=0` 和容量大小的 `batch_size`,之后 `states.layer_idx`/`states.batch_size` 不会再更新。下游必须使用 `AFDAsyncFFNWorkItem.layer_idx`/`num_tokens` 或 `metadata.layer_idx`,不能读 `states` 里的这两个值。\n- `hidden_states` 和 `states.dynamic_scales` 必须切到同一个 `num_tokens`。\n- 三个标量目前通过一次 `torch.stack(...).cpu()` 读取。新增的 host 读值应并入这次读取,不要再加一次 D2H 同步。\n\n\n\n## AFD-I59 — async CAM:零 token 的 FFN work item 也必须 combine-send,并且用浮点占位张量\n\n- 每个 `AFDAsyncFFNWorkItem` 都要走一次 `send_ffn_work_item_output`。`num_tokens == 0` 时如果跳过发送,Attention 侧的 `afd_async_combine_recv` 会一直收不到数据。\n- 零 token 分支发送的是形状 `(1, hidden)`、dtype 为 `self.activation_dtype` 的全零张量。\n- 这里不能复用 `recv_output.hidden_states`:开启 `dynamicQuant` 时它是 int8,CAM combine-send 不接受。\n- 该方法可能返回替换后的 `ffn_output`。调用方如果还要继续处理,必须用返回值,而不是原来的输入。\n- `_send_ffn_output_payload` 只发送 `AFDF2ATransferPayload.routed_output`。给 payload 新增的字段不会经 CAM 回传,需要单独处理。\n\n\n\n## AFD-I63 — CAMP2P:进程组创建顺序和 ubatch→HCCL group 映射\n\n- 所有 rank 都在同一个 `tcp://host:port` 上 rendezvous,所以各 rank 执行 `init_afd_connector` 的调用序列必须一致:\n 1. 先按 ubatch 创建 HCCL 组 `afd`、`afd1`、…,world 大小为 ffn + attn;\n 2. 再由 FFN 单独创建 `afd_moe`;\n 3. 最后由参与者创建 gloo 组 `p2p`。\n- 改变顺序、改组名,或者只在一种角色上加组,都会让 rendezvous 卡住。Attention 和 FFN 两侧的 `parallel_config.num_ubatches` 必须相同。\n- 算子只接收三个组名。`_get_group_ep` 把 ubatch 0/1/2 映射到 `hccl_comm_name`/`hccl_comm_name2`/`hccl_comm_name3`:\n - 只有一个 ubatch 时,ubatch 1 退回组 0;\n - 缺第三组时,ubatch 2 直接报错;\n - ubatch ≥3 会静默落回组 0。\n 要支持更多 ubatch,必须同时修改 custom op 签名和 `_get_group_ep`。\n- 命名陷阱:属性 `self.hccl_comm_name1` 是 `afd_moe` 组(写入 `cam_p2p_ep_name`),而 `_get_group_ep` 的形参 `hccl_comm_name1` 是主 `afd` 组。调用时第一个参数必须是 `self.hccl_comm_name`。\n\n\n\n## AFD-I64 — CAMP2P DP 元数据组:发送目的地与接收 src 公式必须互为逆映射\n\n- 发送侧:前 `min_size` 个 Attention rank 以 `p2p_rank = role_rank + min(ffn_size, attention_size)` 向 `dp_metadata_destinations` 发送。\n- 接收侧:FFN 以 `src = p2p_rank % min_size + ffn_size` 接收。\n- 这组公式依赖 `attention_size >= ffn_size`,此时 `min_size == ffn_size`。放宽 `build_camp2p_topology` 里的这项校验,src 公式和 destinations 就不再对应。\n- `participates_in_p2p_group` 选出的 rank 数必须等于 `p2p_world_size = ffn_size + min_size`,否则 gloo 组 rendezvous 会挂住。\n- 这个组用的是 gloo backend,`send_dp_metadata_list`/`recv_dp_metadata_list` 的 wire 张量必须留在 CPU。\n- 非发送者 rank 调用 `send_dp_metadata_list` 会静默返回。\n\n\n\n## AFD-I65 — CAMP2P custom op 通过 forward context 传递状态,同一 context 内只能有一个未完成的 send→recv\n\n- `send_attn_output` 在调用 `torch.ops.vllm.afd_camp2p_send_attn_output` 之前,写入新的 `forward_context.cam_afdtransfer_state` 和 `ubatch_idx`。\n- op impl 用 `ubatch_idx` 选组,并把 A2E 的 `outputs[3]` 写回 `atten_batch_size`。`recv_ffn_output_impl` 依赖这个值。\n- 每个 forward context 只有一个状态槽位。同一 context 内第二次 `send_attn_output` 会覆盖前一次的 `atten_batch_size`,之后再 recv 前一个 ubatch 就会用错数据。如果要让多个 ubatch 重叠,需要按 ubatch 分开存状态。\n- op 本身不接收 ubatch 参数,所以 `recv_ffn_output` 必须先设置 `get_forward_context().ubatch_idx`,再调用 op。\n- 修改 op 签名时,要同步修改 `*_impl`、`*_fake_impl`、`send_annotations`/`recv_annotations` 和所有调用处。\n- 注册受 `_CAMP2P_CUSTOM_OPS_REGISTERED` 保护,并且会吞掉重复注册错误。如果同名 op 已经注册过,改签名不会有显式报错。\n- `send_attn_output` 里新增的 host 侧校验,要像现有 shape 校验一样在 `torch.compiler.is_compiling()` 下跳过,以保留 `FULL_DECODE_ONLY` ACL graph 路径。\n\n\n\n## AFD-I66 — CAMP2P ids 通道:Attention 的 `compute_gate` 必须与 FFN 的 `recv_input_ids` 成对选择\n\n- Attention 传入 `input_ids` 时使用 `compute_gate=1`。FFN 必须用 `recv_attn_output(recv_input_ids=True)` 选择同一种算子模式。\n- 模式必须由调用方显式声明,不能改成按返回张量的形状推断,因为真实的单 token 层和算子占位张量无法区分。\n- 模式 0 下 A2E 的 `outputs[1]` 是未初始化内存,只能在 `compute_gate_mode == 1` 时读取。\n- `prepare_token_id_transfer` 的约束:ids 数必须恰好等于 `metadata.total_tokens`,列数为 `num_experts_per_tok`,scales 全为零(这个通道只传 token 身份,不传路由权重)。\n- `received_token_ids` 的约束:收到的数量必须 ≥ FFN 实际计算的 token 数,然后截断到该值。\n- 放宽这两处校验,按 token 选专家的 router 会给错误的 token 选专家。\n\n\n\n## AFD-I62 — `connector_extra_config` 白名单、`from_mapping`、`to_mapping` 与 dataclass 字段必须同步修改\n\n- `from_mapping` 用 `_AFD_ASYNC_EXTRA_CONFIG_FIELDS` 和 `_CAMP2P_EXTRA_CONFIG_FIELDS` 拒绝未知 key。新字段如果只加到 dataclass、没加进白名单,用户配置会直接报 unknown field。\n- 外部 key 名属于对外契约:\n - `dynamicQuant` 对应属性 `dynamic_quant`;\n - `ATTN_RANKS_PER_DP_CONFIG_KEY` 必须与白名单里的 `attn_ranks_per_dp` 一致。\n- `to_mapping` 会省略值为 `None` 的可选字段。新增字段要保证 `from_mapping(x.to_mapping()) == x`。\n- 新字段要经过 `coerce_extra_*` 系列函数做类型校验,不能直接用 `raw.get(...)` 的结果。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-deepen-AFD-I62", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I62: evidence swapped for AFD-I36's (another module)", + "rule_id": "AFD-I62", + "source": "synthetic", + "stage": "deepen", + "synthetic": true, + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I1.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I1.json new file mode 100644 index 00000000..9a13bd60 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I1.json @@ -0,0 +1,34 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L211-L224", + "text": " @property\n def p2p_world_size(self) -> int:\n \"\"\"Return the number of FFN and participating Attention metadata ranks.\"\"\"\n return self.ffn_size + self.min_size\n\n @property\n def participates_in_p2p_group(self) -> bool:\n \"\"\"Return whether this process joins the Gloo DP-metadata group.\"\"\"\n return self.world_rank < self.ffn_size or self.is_attn_top_min_size_rank\n\n @property\n def is_attn_top_min_size_rank(self) -> bool:\n \"\"\"Return whether this is an Attention metadata-sender rank.\"\"\"\n return self.ffn_size <= self.world_rank < self.ffn_size + self.min_size" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L789-L817", + "text": " def send_dp_metadata_list(\n self,\n payload: AFDControlPayload,\n ) -> None:\n connector = self.connector\n if connector.p2p_pg is None:\n return\n if not connector.topology.is_attn_top_min_size_rank:\n return\n # The CAMP2P DP-metadata group runs on gloo, so the wire tensors stay on\n # CPU rather than the NPU device.\n device = torch.device(\"cpu\")\n send_control_payload(\n payload,\n dst=connector.dst_list,\n group=connector.p2p_pg,\n device=device,\n )\n\n def recv_dp_metadata_list(self) -> AFDControlPayload:\n connector = self.connector\n if connector.p2p_pg is None:\n raise RuntimeError(\"CAMP2P metadata process group is not initialized\")\n src = connector.p2p_rank % connector.min_size + connector.ffn_size\n return recv_control_payload(\n src=src,\n group=connector.p2p_pg,\n device=torch.device(\"cpu\"),\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L840-L877", + "text": " attention_size, ffn_size = topology_from_config(afd_config)\n if attention_size <= 0 or ffn_size <= 0:\n raise ValueError(\"CAMP2P topology sizes must be positive\")\n if attention_size < ffn_size:\n raise ValueError(\n \"CAMP2P requires attention_size >= ffn_size, got \"\n f\"{attention_size} < {ffn_size}\",\n )\n if role_rank < 0:\n raise ValueError(f\"CAMP2P role rank must be non-negative, got {role_rank}\")\n\n if afd_config.role == \"attention\":\n if role_rank >= attention_size:\n raise ValueError(\n \"Attention role rank must be within attention size \"\n f\"(rank={role_rank}, size={attention_size})\",\n )\n world_rank = ffn_size + role_rank\n p2p_rank = role_rank + min(ffn_size, attention_size)\n elif afd_config.role == \"ffn\":\n if role_rank >= ffn_size:\n raise ValueError(\n \"FFN role rank must be within FFN size \"\n f\"(rank={role_rank}, size={ffn_size})\",\n )\n world_rank = role_rank\n p2p_rank = role_rank\n else:\n raise ValueError(f\"unknown AFD role {afd_config.role!r}\")\n\n min_size = min(attention_size, ffn_size)\n destinations: list[int] = []\n if ffn_size <= world_rank < ffn_size + min_size:\n local_attention_rank = world_rank - ffn_size\n dst = local_attention_rank\n while dst < ffn_size:\n destinations.append(dst)\n dst += min_size" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-skeleton-AFD-I1", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I1: evidence swapped for AFD-I64's (another module)", + "rule_id": "AFD-I1", + "source": "synthetic", + "stage": "skeleton", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I10.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I10.json new file mode 100644 index 00000000..9036afde --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I10.json @@ -0,0 +1,69 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L21-L24", + "text": "The target runtime is **vLLM `v0.26.0`**. The plugin does not modify the vLLM\nsource tree. AFD behavior is installed through the `vllm.general_plugins` entry\npoint, `--additional-config`, automatically selected role workers, plugin-owned\nmodel wrappers, and narrow version-scoped compatibility shims." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L67-L67", + "text": "- vLLM versions other than `0.26.0` are not claimed as supported." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L106-L106", + "text": "The optional extra pins `vllm==0.26.0`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L114-L119", + "text": "| Component | Version |\n| --- | --- |\n| Python | `3.10` or `3.11` |\n| vLLM | `0.26.0` |\n| vLLM-Ascend | commit [`80d8c194f`](https://github.com/vllm-project/vllm-ascend/commit/80d8c194f7584b17fe08065ea99a130916f6b0e7) |\n| CANN / torch / torch-npu | Use the mutually compatible versions required by that vLLM-Ascend source snapshot. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L61-L71", + "text": "The package extra pins vLLM `0.26.0`, and\n[`compat/vllm.py`](../../../afd_plugin/compat/vllm.py) enforces the same target.\nDirect strict calls raise for a missing or different vLLM; plugin registration\ncalls the check with `strict=False`, so it warns and continues. This warning\npolicy does not make another vLLM release supported.\n\nThe NPU v0.26 refresh is based on vLLM-Ascend commit\n[`80d8c194f`](https://github.com/vllm-project/vllm-ascend/commit/80d8c194f7584b17fe08065ea99a130916f6b0e7).\nThe repository does not declare a vLLM-Ascend package dependency in\n`pyproject.toml`, so this source commit and the recorded NPU validation are the\ncompatibility evidence rather than a released package or container tag." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L119-L125", + "text": "| [`async_dp_engine.py`](../../../afd_plugin/compat/patches/async_dp_engine.py): `EngineCoreProc.run_engine_core`, `vllm.v1.engine.utils.launch_core_engines` and its imported client alias, `DPCoordinatorProc.run_coordinator`, `DPCoordinatorProc.process_input_socket`, `DPAsyncMPClient.add_request_async` | Async-DP Attention selects regular `EngineCoreProc`, keeps asynchronous coordinator stats but disables DP wave coordination and `FIRST_REQ`; other configs run the copied upstream branches. | Imported by `register_afd`; general async bindings keep the target/dev guard, while the copied coordinator is exact-`0.26.0` only. Its plugin-owned process target reapplies the loop in spawn children; newer development versions retain their native coordinator. | [`test_async_dp_engine.py`](../../../tests/unit/compat/patches/test_async_dp_engine.py) covers spawn import, exact-version isolation, process selection, non-AFD MoE DP, coordinator mode, wakeup, and reload. | Remove when vLLM exposes role-selectable async-DP engine scheduling, wave policy, and wakeup hooks. |\n| [`async_dp_forward_context.py`](../../../afd_plugin/compat/patches/async_dp_forward_context.py): `vllm.forward_context.set_forward_context` plus already-imported worker aliases | Skips native `DPMetadata` construction/coordination only for AFD async-DP; otherwise uses the copied upstream flow. | Imported by `register_afd`; same target/dev/unknown guard. Rebinds known already-imported aliases so callers do not retain the old function. | [`test_async_dp_forward_context.py`](../../../tests/unit/compat/patches/test_async_dp_forward_context.py) covers async skip and non-async coordination. | Remove when vLLM supports a per-engine-role opt-out from native MoE DP metadata coordination. |\n| [`config_validation.py`](../../../afd_plugin/compat/patches/config_validation.py): `EngineArgs.create_engine_config`, `VllmConfig.__post_init__` | For AFD-owned ubatching with a non-DeepEP backend, temporarily presents `deepep_low_latency` during upstream validation and restores the configured backend. After upstream platform normalization, maps an initial `worker_cls=\"auto\"` to the role-specific CUDA or standard Ascend AFD worker. | Imported by `register_afd`; accepts the target version, development versions, or missing version metadata. Saves originals on upstream modules under AFD-specific attributes before installing wrappers. Explicit worker paths and non-AFD configs are not remapped. | [`test_config_validation.py`](../../../tests/unit/compat/patches/test_config_validation.py) covers backend relaxation, four role/platform mappings, explicit and non-AFD preservation, repeated validation, unsupported platforms, and dev versions. | Remove the backend branch when upstream validation distinguishes plugin-owned ubatching; remove worker mapping when vLLM offers plugin-owned role-aware worker selection. |\n| [`engine_core.py`](../../../afd_plugin/compat/patches/engine_core.py): `EngineCore.__init__`, `_initialize_kv_caches`, `shutdown`; `EngineCoreProc.run_busy_loop`; `DPEngineCoreProc.run_busy_loop` | AFD FFN becomes a connector daemon: construct executor, skip scheduler/KV setup, return an empty KV-shaped result on late paths, start/monitor/stop the FFN worker loop, and use FFN-safe shutdown. Non-FFN branches copy pinned upstream behavior. | Imported by `register_afd`; **no patch-local version guard and no saved-original sentinel**. Direct class assignment means the package pin and review discipline are the compatibility guard. | [`test_engine_core.py`](../../../tests/unit/compat/patches/test_engine_core.py) covers FFN initialization, non-FFN behavior, and daemon start/stop; role runtime tests cover error propagation. | Remove when vLLM offers a headless connector-daemon engine lifecycle or an executor mode that does not require scheduler/KV ownership. |\n| [`npu/ascend_platform.py`](../../../afd_plugin/compat/patches/npu/ascend_platform.py): `NPUPlatform.check_and_update_config` | Snapshots AFD DBO state, runs upstream normalization, and restores configured `enable_dbo`, `ubatch_size`, and `all2all_backend` in `finally`; non-AFD behavior is unchanged. | Installed through the config facade during AFD NPU normalization and verified again by the worker runtime facade; no version guard. Saves the original on the class and uses a class sentinel. The runtime facade caches success only after the wrapper is installed, so an early missing vLLM-Ascend import remains retryable. | [`test_runtime.py`](../../../tests/unit/compat/test_runtime.py) and [`test_npu_runtime.py`](../../../tests/unit/v1/worker/test_npu_runtime.py). | Remove when vLLM-Ascend recognizes plugin-owned DBO workers or no longer clears these fields. |\n| [`npu/mla_graph.py`](../../../afd_plugin/compat/patches/npu/mla_graph.py): `vllm_ascend.attention.mla_v1.get_graph_params` | Resolves an AFD ubatch-owned `GraphParams` registry from the active forward context and otherwise delegates to the saved upstream process-global resolver. | Called through `apply_afd_ascend_patches_if_needed` during NPU worker startup; no version guard. Saves the original on the upstream module and uses a module sentinel. | [`test_runtime.py`](../../../tests/unit/compat/test_runtime.py) covers AFD-context resolution, upstream fallback, and idempotence; [`test_npu_mla_graph.py`](../../../tests/unit/v1/worker/test_npu_mla_graph.py) covers the owning graph lifecycle. | Remove when vLLM-Ascend accepts a forward-context-local MLA graph registry or exposes an equivalent resolver hook. |\n| [`npu/force_load_balance.py`](../../../afd_plugin/compat/patches/npu/force_load_balance.py): `AscendW8A8DynamicFusedMoEMethod.__init__`, `AscendW8A8DynamicFusedMoEMethod.apply` | Captures AFD profiling configuration as method-owned state and replaces routed expert IDs with a deterministic balanced buffer only when the method-owned switch is enabled; normal model-selected routing remains unchanged. This switch changes outputs and is not a correctness feature. | Imported by the NPU FFN worker after vLLM-Ascend platform initialization; **no patch-local version guard or explicit reload sentinel**. Functions copy the current upstream bodies with marked AFD deltas. | [`test_force_load_balance.py`](../../../tests/unit/compat/patches/test_force_load_balance.py) covers buffer bounds, determinism, growth, override, and pass-through. | Upstream a deterministic expert-routing profiling hook in vLLM-Ascend, then delete both copied functions. |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L138-L149", + "text": "| [`v1/worker/attention_model_runner.py`](../../../afd_plugin/v1/worker/attention_model_runner.py) | Disables vLLM 0.26's block-table-only Attention metadata cache only while multiple DBO ubatches are built, then restores every affected builder in `finally`. Remove this workaround when [vLLM #48659](https://github.com/vllm-project/vllm/pull/48659), or an equivalent ubatch-aware cache key, reaches the pinned release. |\n\nSeveral scoped upstream adaptations live with their semantic owners rather\nthan this patch directory. Model dummy runs and V2 execution temporarily wrap\n`create_forward_context`; CUDA worker startup temporarily substitutes the V1\nor V2 runner symbol; V2 graph capture temporarily wraps native input\npreparation; and full-graph replay installs an instance-local manager hook.\nEvery scope restores symbols and runner state in `finally`. Connector process\ngroups also call private PyTorch/vLLM helpers as described in\n[connector contracts](connector_contracts.md). All of these seams must be\nincluded in an upstream upgrade audit even though they are not process-global\nimport-time patches." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L167-L169", + "text": "When the pinned upstream version changes, copied bodies must be refreshed from\nthat version before reapplying marked AFD deltas. Passing unit tests against a\ndifferent signature is not sufficient compatibility evidence." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/compatibility_and_patches.md:L197-L209", + "text": "- `PATCH-INV-003`: unsupported upstream versions are not treated as supported\n merely because best-effort registration continued.\n- `PATCH-INV-004`: patch initialization is idempotent or explicitly guarded,\n and partial application is observable before the affected runtime executes.\n\n## Upstream relationship and validation requirements\n\nPatch signatures and copied code must match their pinned upstream symbols.\nEvery modification requires focused tests for AFD and non-AFD behavior plus\nthe architectural review required by `AGENTS.md`. Version guard changes also\nrequire package compatibility tests. Ascend patch refreshes require an exact\nvLLM-Ascend source reference and the NPU unit/E2E evidence for the affected\npath." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/connector_contracts.md:L270-L273", + "text": "The common process-group helper creates a plugin-owned group while temporarily\nswitching PyTorch's default group so vLLM parallel-state helpers initialize\nagainst it. It currently calls private PyTorch/vLLM symbols and is therefore an\nupgrade-sensitive compatibility boundary, not a third-party process-group API." + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-skeleton-AFD-I10", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I10: evidence swapped for AFD-I5's (another module)", + "rule_id": "AFD-I10", + "source": "synthetic", + "stage": "skeleton", + "synthetic": true, + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I12.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I12.json new file mode 100644 index 00000000..88cdf54f --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I12.json @@ -0,0 +1,54 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L147-L148", + "text": " graph_key = make_ffn_graph_key(dp_metadata_list)\n cuda_graph_info = self._cuda_graphs.get(graph_key)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L306-L306", + "text": " graph_key = make_ffn_graph_key(dp_metadata_list)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L322-L326", + "text": " self._cuda_graphs[graph_key] = {\n \"graph\": cudagraph,\n \"input_hidden_states\": output,\n \"output\": output,\n }" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L223-L225", + "text": " graph_key = self._make_graph_key(dp_metadata_list)\n acl_graphs = self._acl_graphs\n graph_info = acl_graphs.get(graph_key)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L256-L265", + "text": " def _make_graph_key(\n self,\n dp_metadata_list: dict[int, DPMetadata | AFDDPMetadata],\n ) -> tuple:\n return make_ffn_graph_key(\n dp_metadata_list,\n attention_size=int(self.connector.attn_size),\n ffn_size=int(self.connector.ffn_size),\n fallback=int(self.max_num_tokens),\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L488-L494", + "text": " graph_key = self._make_graph_key(dp_metadata_list)\n if graph_key in self._acl_graphs:\n logger.debug(\n \"AFD NPU FFN ACL graph capture skipped for existing key=%s\",\n graph_key,\n )\n return" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L518-L521", + "text": " self._acl_graphs[graph_key] = {\n \"graph\": graph,\n \"output\": output,\n }" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-skeleton-AFD-I12", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I12: evidence swapped for AFD-I45's (another module)", + "rule_id": "AFD-I12", + "source": "synthetic", + "stage": "skeleton", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I16.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I16.json new file mode 100644 index 00000000..ffe9fddd --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I16.json @@ -0,0 +1,44 @@ +{ + "base": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L128-L159", + "text": " def _send_and_receive(\n self,\n hidden_states: torch.Tensor,\n **send_kwargs: torch.Tensor,\n ) -> torch.Tensor:\n afd_metadata = get_afd_metadata_from_forward_context()\n if afd_metadata is None:\n raise RuntimeError(\"RemoteFFNProxy requires AFD forward metadata\")\n forward_context = get_forward_context()\n stage_idx = int(\n getattr(forward_context, \"ubatch_idx\", afd_metadata.stage_idx),\n )\n afd_metadata.stage_idx = stage_idx\n metadata = AFDTransferMetadata.create_attention_metadata(\n layer_idx=self.layer_idx,\n stage_idx=stage_idx,\n seq_len=int(hidden_states.shape[0]),\n )\n context = AFDTransferContext(metadata=metadata)\n afd_metadata.connector.send_attn_output(\n hidden_states,\n context,\n **send_kwargs,\n )\n hidden_states = maybe_apply_dbo_yield(\n hidden_states,\n role=\"attention\",\n )\n return afd_metadata.connector.recv_ffn_output(\n ref_tensor=hidden_states,\n ubatch_idx=stage_idx,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v2.py:L176-L189", + "text": " def forward( # type: ignore[override]\n self,\n hidden_states: torch.Tensor,\n router_logits: torch.Tensor,\n input_ids: torch.Tensor | None = None,\n ) -> torch.Tensor:\n if input_ids is not None:\n raise NotImplementedError(\n \"experts-boundary input_ids transport is not implemented\",\n )\n send_kwargs = (\n {} if self.is_internal_router else {\"router_logits\": router_logits}\n )\n return self._send_and_receive(hidden_states, **send_kwargs)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v4.py:L82-L120", + "text": " def forward(\n self,\n hidden_states: torch.Tensor,\n input_ids: torch.Tensor | None,\n ) -> torch.Tensor:\n if input_ids is None:\n raise RuntimeError(\"DeepSeek-V4 remote FFN requires input_ids\")\n if input_ids.ndim != 1 or input_ids.shape[0] != hidden_states.shape[0]:\n raise ValueError(\n \"DeepSeek-V4 input_ids must be one-dimensional and token-aligned\",\n )\n\n afd_metadata = get_afd_metadata_from_forward_context()\n if afd_metadata is None:\n raise RuntimeError(\"RemoteDeepseekV4FFN requires AFD forward metadata\")\n forward_context = get_forward_context()\n stage_idx = int(\n getattr(forward_context, \"ubatch_idx\", afd_metadata.stage_idx),\n )\n afd_metadata.stage_idx = stage_idx\n metadata = AFDTransferMetadata.create_attention_metadata(\n layer_idx=self.layer_idx,\n stage_idx=stage_idx,\n seq_len=int(hidden_states.shape[0]),\n )\n context = AFDTransferContext(metadata=metadata)\n afd_metadata.connector.send_attn_output(\n hidden_states,\n context,\n input_ids=input_ids,\n )\n hidden_states = maybe_apply_dbo_yield(\n hidden_states,\n role=\"attention\",\n )\n return afd_metadata.connector.recv_ffn_output(\n ref_tensor=hidden_states,\n ubatch_idx=stage_idx,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v4.py:L302-L313", + "text": " def compute_ffn_output(\n self,\n hidden_states: torch.Tensor,\n *,\n input_ids: torch.Tensor | None,\n ) -> torch.Tensor:\n \"\"\"Execute the complete native V4 MoE, including its native router.\"\"\"\n if not isinstance(self.ffn, native.DeepseekV4MoE):\n raise RuntimeError(\"DeepSeek-V4 FFN compute is FFN-role only\")\n if input_ids is None:\n raise RuntimeError(\"DeepSeek-V4 FFN compute requires input_ids\")\n return self.ffn(hidden_states, input_ids)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/model_executor/models/deepseek_v4.py:L476-L499", + "text": "class AFDDeepseekV4ForCausalLM(native.DeepseekV4ForCausalLM):\n \"\"\"DeepSeek-V4 causal LM exposing the GPU FFN-runner model contract.\"\"\"\n\n model_cls = AFDDeepseekV4Model\n afd_requires_input_ids = True\n\n def __init__(self, *, vllm_config: VllmConfig, prefix: str = \"\") -> None:\n self.afd_config = parse_afd_config(vllm_config, validate=False)\n self.afd_role = self.afd_config.role\n super().__init__(vllm_config=vllm_config, prefix=prefix)\n\n def compute_ffn_output(\n self,\n hidden_states: torch.Tensor,\n layer_idx: int,\n *,\n input_ids: torch.Tensor | None = None,\n **kwargs: Any,\n ) -> torch.Tensor:\n return self.model.compute_ffn_output(\n hidden_states,\n layer_idx,\n input_ids=input_ids,\n )" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-skeleton-AFD-I16", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I16: evidence swapped for AFD-I21's (another module)", + "rule_id": "AFD-I16", + "source": "synthetic", + "stage": "skeleton", + "synthetic": true, + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I17.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I17.json new file mode 100644 index 00000000..a5a90c7e --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I17.json @@ -0,0 +1,44 @@ +{ + "base": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L531-L575", + "text": " if not torch.compiler.is_compiling() and not metadata.validate_tensor_shape(\n tuple(hidden_states.shape),\n ):\n raise ValueError(\n f\"hidden_states shape {hidden_states.shape!r} does not match \"\n f\"CAMP2P metadata token count {metadata.total_tokens}\",\n )\n input_ids = cast(torch.Tensor | None, kwargs.get(\"input_ids\"))\n expert_ids: torch.Tensor | None = None\n expert_scales: torch.Tensor | None = None\n compute_gate = 0\n if input_ids is not None:\n expert_ids, expert_scales = prepare_token_id_transfer(\n input_ids,\n topk=self.num_experts_per_tok,\n expected_tokens=metadata.total_tokens,\n )\n compute_gate = 1\n transfer_state = CAMP2PTransferState(\n aiv_num=self.aiv_num,\n batch_size=metadata.total_tokens,\n h=self.hidden_size,\n k=self.num_experts_per_tok,\n )\n ubatch_idx = metadata.stage_idx\n forward_context = get_forward_context()\n forward_context.cam_afdtransfer_state = transfer_state\n forward_context.ubatch_idx = ubatch_idx\n\n torch.ops.vllm.afd_camp2p_send_attn_output(\n hidden_states,\n self.hccl_comm_name,\n self.hccl_comm_name2,\n self.hccl_comm_name3,\n transfer_state.batch_size,\n transfer_state.h,\n transfer_state.k,\n self.ffn_size,\n self.attn_size,\n self.world_rank,\n transfer_state.aiv_num,\n compute_gate,\n expert_ids,\n expert_scales,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L601-L617", + "text": " transfer_state = getattr(get_forward_context(), \"cam_afdtransfer_state\", None)\n if transfer_state is None:\n raise RuntimeError(\"CAMP2P Attention side is missing connector data\")\n get_forward_context().ubatch_idx = ubatch_idx\n return torch.ops.vllm.afd_camp2p_recv_ffn_output(\n ref_tensor,\n self.hccl_comm_name,\n self.hccl_comm_name2,\n self.hccl_comm_name3,\n transfer_state.batch_size,\n transfer_state.h,\n transfer_state.k,\n self.ffn_size,\n self.attn_size,\n self.world_rank,\n transfer_state.aiv_num,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L982-L1013", + "text": " transfer_state = getattr(get_forward_context(), \"cam_afdtransfer_state\", None)\n if transfer_state is None:\n transfer_state = CAMP2PTransferState()\n transfer_state.batch_size = batch_size\n transfer_state.h = hidden_size\n transfer_state.k = topk\n transfer_state.aiv_num = aiv_num\n group_ep = _get_group_ep(\n int(getattr(get_forward_context(), \"ubatch_idx\", 0)),\n hccl_comm_name,\n hccl_comm_name2,\n hccl_comm_name3,\n )\n\n outputs = torch.ops.afd_ascend.a2e(\n hidden_states,\n expert_ids,\n expert_scales,\n transfer_state.batch_size,\n transfer_state.h,\n transfer_state.k,\n ffn_size,\n attn_size,\n world_rank,\n group_ep,\n transfer_state.aiv_num,\n compute_gate,\n )\n transfer_state.atten_batch_size = outputs[3]\n forward_context = get_forward_context()\n forward_context.cam_afdtransfer_state = transfer_state\n return hidden_states" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L1047-L1071", + "text": " transfer_state = getattr(get_forward_context(), \"cam_afdtransfer_state\", None)\n if transfer_state is None or transfer_state.atten_batch_size is None:\n raise RuntimeError(\"CAMP2P Attention side is missing A2E handle data\")\n transfer_state.batch_size = batch_size\n transfer_state.h = hidden_size\n transfer_state.k = topk\n transfer_state.aiv_num = aiv_num\n group_ep = _get_group_ep(\n int(getattr(get_forward_context(), \"ubatch_idx\", 0)),\n hccl_comm_name,\n hccl_comm_name2,\n hccl_comm_name3,\n )\n output = torch.ops.afd_ascend.e2a(\n ref_tensor,\n transfer_state.atten_batch_size,\n transfer_state.batch_size,\n transfer_state.h,\n transfer_state.k,\n ffn_size,\n attn_size,\n world_rank,\n group_ep,\n transfer_state.aiv_num,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/camp2p.py:L1090-L1149", + "text": " send_annotations = {\n \"hidden_states\": torch.Tensor,\n \"hccl_comm_name\": str,\n \"hccl_comm_name2\": str,\n \"hccl_comm_name3\": str,\n \"batch_size\": int,\n \"hidden_size\": int,\n \"topk\": int,\n \"ffn_size\": int,\n \"attn_size\": int,\n \"world_rank\": int,\n \"aiv_num\": int,\n \"compute_gate\": int,\n \"expert_ids\": torch.Tensor | None,\n \"expert_scales\": torch.Tensor | None,\n \"return\": torch.Tensor,\n }\n recv_annotations = {\n \"ref_tensor\": torch.Tensor,\n \"hccl_comm_name\": str,\n \"hccl_comm_name2\": str,\n \"hccl_comm_name3\": str,\n \"batch_size\": int,\n \"hidden_size\": int,\n \"topk\": int,\n \"ffn_size\": int,\n \"attn_size\": int,\n \"world_rank\": int,\n \"aiv_num\": int,\n \"return\": torch.Tensor,\n }\n send_attn_output_impl.__annotations__ = send_annotations\n send_attn_output_fake_impl.__annotations__ = send_annotations\n recv_ffn_output_impl.__annotations__ = recv_annotations\n recv_ffn_output_fake_impl.__annotations__ = recv_annotations\n\n try:\n direct_register_custom_op(\n op_name=\"afd_camp2p_send_attn_output\",\n op_func=send_attn_output_impl,\n mutates_args=[],\n fake_impl=send_attn_output_fake_impl,\n dispatch_key=\"PrivateUse1\",\n )\n direct_register_custom_op(\n op_name=\"afd_camp2p_recv_ffn_output\",\n op_func=recv_ffn_output_impl,\n mutates_args=[],\n fake_impl=recv_ffn_output_fake_impl,\n dispatch_key=\"PrivateUse1\",\n )\n except RuntimeError as exc:\n message = str(exc).lower()\n duplicate = any(\n marker in message\n for marker in (\"already\", \"duplicate\", \"same name\", \"defined\")\n )\n if not duplicate:\n raise\n _CAMP2P_CUSTOM_OPS_REGISTERED = True" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-skeleton-AFD-I17", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I17: evidence swapped for AFD-I65's (another module)", + "rule_id": "AFD-I17", + "source": "synthetic", + "stage": "skeleton", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I19.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I19.json new file mode 100644 index 00000000..2123d931 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I19.json @@ -0,0 +1,24 @@ +{ + "base": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/connectors/npu/async_cam.py:L394-L454", + "text": " def _send_ffn_output_payload(\n self,\n ffn_output: Tensor | AFDF2ATransferPayload,\n context: AFDTransferContext,\n *,\n stage_idx: int,\n ) -> None:\n if not isinstance(ffn_output, AFDF2ATransferPayload):\n self.send_ffn_output(\n ffn_output,\n context,\n ubatch_idx=stage_idx,\n )\n return\n\n self.send_ffn_output(\n ffn_output.routed_output,\n context,\n ubatch_idx=stage_idx,\n )\n\n def send_ffn_work_item_output(\n self,\n work_item: AFDAsyncFFNWorkItem,\n ffn_output: Tensor | AFDF2ATransferPayload,\n ) -> Tensor | AFDF2ATransferPayload:\n \"\"\"Return one FFN work item's routed output through CAM.\n\n A one-token floating routed placeholder is used when a rank receives no\n routed tokens because CAM combine-send cannot consume the dynamic\n quantized dispatch buffer as an empty routed result.\n \"\"\"\n if work_item.num_tokens > 0:\n self._send_ffn_output_payload(\n ffn_output,\n work_item.context,\n stage_idx=work_item.stage_idx,\n )\n return ffn_output\n\n # Temporary NPU workaround: CAM combine-send does not accept the int8\n # dispatch-recv buffer as routed output. When this FFN rank has no\n # routed tokens, send one fake floating routed token instead of reusing the\n # dynamicQuant int8 input buffer.\n fake_routed_output = torch.zeros(\n (1, work_item.recv_output.hidden_states.shape[-1]),\n dtype=self.activation_dtype,\n device=work_item.recv_output.hidden_states.device,\n )\n if isinstance(ffn_output, AFDF2ATransferPayload):\n ffn_output = AFDF2ATransferPayload(\n routed_output=fake_routed_output,\n )\n else:\n ffn_output = fake_routed_output\n self._send_ffn_output_payload(\n ffn_output,\n work_item.context,\n stage_idx=work_item.stage_idx,\n )\n return ffn_output" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-skeleton-AFD-I19", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I19: evidence swapped for AFD-I59's (another module)", + "rule_id": "AFD-I19", + "source": "synthetic", + "stage": "skeleton", + "synthetic": true, + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I2.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I2.json new file mode 100644 index 00000000..3d4fa621 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I2.json @@ -0,0 +1,49 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_metadata.py:L31-L36", + "text": " connector: AFDConnectorBase\n vllm_config: VllmConfig\n _is_warmup: bool\n _afd_transaction_counter: int\n _afd_is_profile: bool = False\n _afd_pending_metadata: AFDForwardContextMetadata | None" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_metadata.py:L226-L229", + "text": " def _next_afd_transaction_id(self) -> str:\n counter = self._afd_transaction_counter\n self._afd_transaction_counter = counter + 1\n return f\"afd-{counter}\"" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L82-L90", + "text": " assert self.connector.control_plane is not None, (\n \"GPU model runner only supports control-plane-driven connectors\"\n )\n self._is_warmup = False\n self._afd_is_graph_capturing = False\n self._afd_is_graph_replaying = False\n self._afd_pending_metadata: AFDForwardContextMetadata | None = None\n self._afd_suppress_metadata_send = False\n self._afd_transaction_counter = 0" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L295-L304", + "text": " if connector.control_plane is None:\n raise RuntimeError(\n \"AFD ModelRunnerV2 requires a control-plane-driven connector\",\n )\n self._is_warmup = False\n self._afd_is_graph_capturing = False\n self._afd_is_graph_replaying = False\n self._afd_pending_metadata: AFDForwardContextMetadata | None = None\n self._afd_suppress_metadata_send = False\n self._afd_transaction_counter = 0" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L102-L107", + "text": "from afd_plugin.v1.worker.attention_metadata import (\n _forward_context_num_tokens,\n _full_cudagraph_padded_tokens,\n _resolve_world_ranks,\n build_ubatch_dp_metadata_list,\n)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L159-L166", + "text": " self._is_warmup = False\n self._afd_is_graph_capturing = False\n self._afd_is_graph_replaying = False\n self._afd_pending_metadata: AFDForwardContextMetadata | None = None\n self._afd_suppress_metadata_send = False\n self._afd_transaction_counter = 0\n self._afd_async_moe_ubatch_metadata: AsyncMoeUbatchMetadata | None = None\n self._afd_live_execution = False" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-skeleton-AFD-I2", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I2: evidence swapped for AFD-I36's (another module)", + "rule_id": "AFD-I2", + "source": "synthetic", + "stage": "skeleton", + "synthetic": true, + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I23.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I23.json new file mode 100644 index 00000000..6a414332 --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I23.json @@ -0,0 +1,44 @@ +{ + "base": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L186-L217", + "text": " experts_layer_indices = frozenset(\n self.model.get_experts_layer_indices(),\n )\n layer_indices = (\n tuple(sorted(experts_layer_indices))\n if self.afd_config.compute_gate_on_attention\n else tuple(range(num_layers))\n )\n stage_ids = sorted(int(stage_idx) for stage_idx in dp_metadata_list) or [0]\n ffn_dp_metadata_list = {\n stage_idx: self._make_ffn_dp_metadata(dp_metadata_list[stage_idx])\n for stage_idx in stage_ids\n }\n recv_input_ids = getattr(self.model, \"afd_requires_input_ids\", False)\n with _ffn_forward_context(self.vllm_config) as forward_context:\n for layer_idx in layer_indices:\n uses_remote_experts = layer_idx in experts_layer_indices\n routing_spec = (\n self.model.get_experts_routing_spec(layer_idx)\n if uses_remote_experts and self.afd_config.compute_gate_on_attention\n else None\n )\n for stage_idx in stage_ids:\n recv_kwargs: dict[str, Any] = {}\n if routing_spec is not None:\n recv_kwargs[\"routing_spec\"] = routing_spec\n if recv_input_ids:\n recv_kwargs[\"recv_input_ids\"] = True\n payload = self.connector.recv_attn_output(\n ubatch_idx=stage_idx,\n **recv_kwargs,\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/ffn_model_runner.py:L246-L246", + "text": " self.connector.send_ffn_output(rank_ffn_output, context)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L303-L304", + "text": " for layer_idx in _ffn_layer_indices(self):\n for stage_idx in stage_ids:" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L565-L586", + "text": "def _ffn_layer_indices(runner: AFDNPUFFNModelRunner) -> range | list[int]:\n num_layers = max(int(runner.num_layers or 0), 1)\n if not runner.afd_config.compute_gate_on_attention:\n return range(num_layers)\n hf_config = runner.model_config.hf_config\n return [\n layer_idx\n for layer_idx in range(num_layers)\n if _is_moe_layer(hf_config, layer_idx)\n ]\n\n\ndef _is_moe_layer(hf_config: object, layer_idx: int) -> bool:\n moe_layer_freq = getattr(hf_config, \"moe_layer_freq\", 1)\n # DeepSeek V4 has routed experts in every decoder layer and does not expose\n # the V2/V3 ``first_k_dense_replace`` compatibility field.\n first_moe_layer = getattr(hf_config, \"first_k_dense_replace\", 0)\n return (\n getattr(hf_config, \"n_routed_experts\", None) is not None\n and layer_idx >= first_moe_layer\n and layer_idx % moe_layer_freq == 0\n )" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/ffn_model_runner.py:L132-L137", + "text": " if tuple(sorted(layer.layer_idx for layer in layers)) != tuple(\n _ffn_layer_indices(self)\n ):\n raise ValueError(\n \"layered GMM weights do not cover the remote MoE layer IDs\"\n )" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/rules-seed-vllm-omni-rebase-upstream-api-drift.md": "---\ntitle: \"上游 API 漂移与兼容边界(afd-plugin)\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# 上游 API 漂移与兼容边界(afd-plugin)\n\n## AFD-I13 — vLLM 目标版本写在多处,注册期只告警:改 pin 时所有守卫要一起改\n\n- 目标版本 `0.26.0` 同时写在三类位置:`pyproject.toml` 的 `vllm` extra(`vllm==0.26.0`)、`afd_plugin/compat/vllm.py` 的版本检查、README 与设计文档里的支持声明。PR 改了其中一处,其余位置必须跟着改。版本守卫有变化时,还要补 package 兼容测试 `tests/unit/package/test_package.py`。\n- 插件注册调用版本检查时用的是 `strict=False`,版本不匹配只打 warning,然后继续运行。因此“新版本上插件能注册”或“单测能通过”都不算支持证据。在签名不同的上游上跑通的单测,也不算兼容证据。\n - `config_validation.py` 和 `async_dp_forward_context.py` 放行三种情况:目标版本、dev 版本、缺失版本元数据。\n - `async_dp_engine.py` 的一般 async 绑定沿用 target/dev 守卫,但其中复制的 coordinator 只在版本恰好是 `0.26.0` 时生效。更新的 dev 版本会保留原生 coordinator。\n- 所以 bump pin 的 PR 必须显式更新这个 exact 守卫及其测试。否则 async-DP Attention 的行为会悄悄变掉,而且不会报错。\n\n\n\n## AFD-I14 — 没有版本守卫的复制型 patch 只靠 pin 和评审兜底:基线一变就要从新上游重新拷贝函数体\n\n- 以下 patch 没有 patch-local 版本守卫:\n - `afd_plugin/compat/patches/engine_core.py` 直接给类属性赋值,也没有 saved-original 哨兵。\n - `afd_plugin/compat/patches/npu/force_load_balance.py` 复制了上游函数体,同样没有版本守卫或重载守卫。\n - `npu/ascend_platform.py` 和 `npu/mla_graph.py` 也没有版本守卫。\n- 任何改变 vLLM 或 vLLM-Ascend 基线的 PR,都要按这个顺序处理:\n - 在 PR 中写明上游文件、symbol、版本或 commit、签名。\n- 全局替换会影响每个加载了插件的进程,所以复制出来的非 AFD 分支也要有回归测试。例如:`tests/unit/compat/patches/test_engine_core.py` 里的非 FFN 路径,`test_force_load_balance.py` 里的 pass-through 路径。\n\n\n\n## AFD-I16 — 靠重绑导入别名生效的 patch:上游新增的 import 点会绕过 AFD 分支\n\n - `async_dp_forward_context.py` 替换 `vllm.forward_context.set_forward_context`,并重绑已知的、已经导入的 worker 别名,防止调用方继续持有旧函数。\n - `async_dp_engine.py` 同时替换 `vllm.v1.engine.utils.launch_core_engines` 和它被导入后的 client 别名。\n- 如果升级后上游在新的模块里导入这些 symbol,新别名不会被重绑。结果是 AFD async-DP 静默退回原生 `DPMetadata` 协调,或退回原生引擎启动流程。\n - 在新基线里 grep 这些 symbol 的全部导入点。\n - 同步更新 `test_async_dp_forward_context.py` 和 `test_async_dp_engine.py`。\n- `async_dp_engine.py` 会在 spawn 子进程里通过插件自有的进程 target 重新应用 loop。新增的重绑点也要覆盖 spawn 导入路径。\n\n\n\n## AFD-I17 — 核心 patch 是 best-effort 分组导入:运行时 fail-fast 检查是唯一兜底,不能删\n\n- `async_dp_engine`、`async_dp_forward_context`、`config_validation`、`engine_core` 在同一个 `try` 里导入。\n- 任一个失败只打 debug 日志,然后继续注册模型。中途失败会留下一个不完整的补丁集:前面的已经装上,后面的被跳过。\n- 部分应用能被发现,全靠运行时的 fail-fast 检查:\n - Attention/FFN 初始化时遇到下列情况会直接失败:AFD config 缺失、角色不匹配、worker class 是隐式的(即 `auto` 没有被解析掉)或错误的。\n - FFN 收到 scheduler 驱动的 `execute_model()` 调用时会 fail fast。如果 `engine_core` 没有生效,这就是最后一道防线。\n- 不得把这些检查当成“不可达代码”删掉,也不得降级成 warning。\n- 往这个 best-effort 块里新增 patch 时,必须同时提供一个显式检查,让受影响的运行时在执行前就能发现问题(PATCH-INV-004),而不是静默产出错误结果。\n\n\n\n## AFD-I18 — CPU-safe 与 Ascend 延迟导入:注册期和公共模块不得引入设备栈\n\n- `vllm` 是可选 extra。CPU 或 macOS 环境必须能用默认的 `uv run pytest` 跑 import/config 测试。\n- 以下模块必须保持 CPU-safe:顶层包、公共配置和校验、版本检查、graph policy helper。其中 `validate_cuda_graph_mode()` 在模块导入时不能 import torch 或 vLLM。\n- `AFDConnectorFactory` 用的是 lazy loader,导入 factory 时不得带出 CUDA 或 Ascend 实现。\n- Ascend 原生算子要等 connector 初始化时才加载。算子缺失应该在 connector 初始化时报错,而不是在导入包时报错。\n- 纯 CUDA 的插件注册不得导入 vLLM-Ascend。Ascend patch 必须等 Ascend 插件完成平台初始化之后再应用,入口是 config facade、worker runtime facade,以及 FFN worker 构造时导入的 force-load-balance。\n- 把 Ascend patch 挪到 `register_afd` 或模块顶层的 PR 应该拒绝。\n- runtime facade 只有在 wrapper 真正装上之后才缓存“成功”,这样早期导入 vLLM-Ascend 失败时还能重试。修改 facade 时要保留这一点。\n\n\n\n## AFD-I19 — vLLM-Ascend 基线只是一个源码 commit(`80d8c194f`),不是包依赖;v0.19.1rc1 的资产不能当 v0.26 的证据\n\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。兼容证据只有两样:源码 commit `80d8c194f`,以及对应的 NPU 验证记录。这个 commit 同时记在 README、`compatibility_and_patches.md`、`execution_platforms.md` 三处。\n- 如果 NPU patch 或 runtime 针对的 vLLM-Ascend 源码快照变了,这三处要一起更新,并附上精确的 commit 和受影响路径的 NPU 单元/E2E 证据。\n- 仓库没有已发布的 v0.26 vLLM-Ascend 容器 tag。不得在 PR 或文档里把某个 tag 或容器写成权威 pin,也不得复用旧的 v0.19.1rc1 镜像作为 v0.26 的运行环境。\n- `CAMAsyncAFDConnector` 的 legacy PCP8 recipe 要求 `release/v0.19.1rc1`。而在 v0.26 上,基于 PCP 的 model-runner-v1 部署不受支持,CAM async 也不支持 prefill/decode 上下文并行。所以引用这个 recipe 得到的结果,不能作为 v0.26 CAM async 的支持证据。\n- NPU 单元测试要在 Ascend 开发环境里跑。默认的 CPU `uv run pytest` 通过,不构成 NPU 证据。\n\n\n\n## AFD-I23 — 显式 worker 路径仍被接受:自动映射和 worker 侧的兜底修正都要保留\n\n- README 有两条承诺:\n - 省略 `--worker-cls` 时,自动选择对应角色的 worker。\n - 显式指定 AFD worker 路径仍然被接受,但不是稳定接口。\n- 自动选择由 `config_validation.py` patch 完成:它在上游平台归一化之后,把初始的 `worker_cls=\"auto\"` 映射成对应的 worker。显式路径和非 AFD 配置不会被重映射。\n- 因此,用显式 worker 路径启动时,会绕过配置期上游对默认 worker 的重写。\n- 对这类旧式启动来说,唯一的兜底是 NPU worker 在设备初始化时做的 non-SP all-to-all backend 修正(选择 `flashinfer_all2allv`,相关逻辑见 `afd_plugin/compat/npu/runtime_config.py`)。不能以“自动选择已经覆盖了”为理由删掉它。\n- 反过来,不得在文档或 recipe 里把显式 worker 类路径写成稳定的启动接口。\n- 新增或修改 worker 类时,要同时更新四种 role/platform 映射及其测试。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-skeleton-AFD-I23", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I23: evidence swapped for AFD-I48's (another module)", + "rule_id": "AFD-I23", + "source": "synthetic", + "stage": "skeleton", + "synthetic": true, + "verdict": "unsure" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I3.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I3.json new file mode 100644 index 00000000..3ce4ddfe --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I3.json @@ -0,0 +1,49 @@ +{ + "base": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_metadata.py:L31-L36", + "text": " connector: AFDConnectorBase\n vllm_config: VllmConfig\n _is_warmup: bool\n _afd_transaction_counter: int\n _afd_is_profile: bool = False\n _afd_pending_metadata: AFDForwardContextMetadata | None" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_metadata.py:L226-L229", + "text": " def _next_afd_transaction_id(self) -> str:\n counter = self._afd_transaction_counter\n self._afd_transaction_counter = counter + 1\n return f\"afd-{counter}\"" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner.py:L82-L90", + "text": " assert self.connector.control_plane is not None, (\n \"GPU model runner only supports control-plane-driven connectors\"\n )\n self._is_warmup = False\n self._afd_is_graph_capturing = False\n self._afd_is_graph_replaying = False\n self._afd_pending_metadata: AFDForwardContextMetadata | None = None\n self._afd_suppress_metadata_send = False\n self._afd_transaction_counter = 0" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/attention_model_runner_v2.py:L295-L304", + "text": " if connector.control_plane is None:\n raise RuntimeError(\n \"AFD ModelRunnerV2 requires a control-plane-driven connector\",\n )\n self._is_warmup = False\n self._afd_is_graph_capturing = False\n self._afd_is_graph_replaying = False\n self._afd_pending_metadata: AFDForwardContextMetadata | None = None\n self._afd_suppress_metadata_send = False\n self._afd_transaction_counter = 0" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L102-L107", + "text": "from afd_plugin.v1.worker.attention_metadata import (\n _forward_context_num_tokens,\n _full_cudagraph_padded_tokens,\n _resolve_world_ranks,\n build_ubatch_dp_metadata_list,\n)" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:afd_plugin/v1/worker/npu/attention_model_runner.py:L159-L166", + "text": " self._is_warmup = False\n self._afd_is_graph_capturing = False\n self._afd_is_graph_replaying = False\n self._afd_pending_metadata: AFDForwardContextMetadata | None = None\n self._afd_suppress_metadata_send = False\n self._afd_transaction_counter = 0\n self._afd_async_moe_ubatch_metadata: AsyncMoeUbatchMetadata | None = None\n self._afd_live_execution = False" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/rules-init.md": "---\ntitle: \"AFD plugin 仓库规则\"\ncreated: 2026-09-30\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin]\nsources: []\n---\n\n# AFD plugin 仓库规则\n\n## AFD-I1 — 新增或重命名 connector 时,必须同时更新 factory 注册、配置白名单和 connector 自有 schema\n\n- 只在 `AFDConnectorFactory` 注册还不够。配置层另有一份硬编码白名单,它不是从注册表推导出来的。名字不在白名单里的 connector,会在配置校验阶段被拒绝。\n- 检查 PR 是否同时更新了以下内容:白名单、README 里 `connector` 可选值的说明、README 的 connector 支持表。\n- 新 connector 必须实现 `parse_extra_config()`,schema 是封闭的,未知字段直接报错。原因是 `compat/npu/feature_validation.py` 会通过 factory 复用同一个 schema 做前置校验,不能在别处另写一套校验。\n- 实现要放进对应的后端包:只用于 GPU 的放 `afd_plugin.connectors.gpu`,只用于 NPU 的放 `afd_plugin.connectors.npu`。\n- factory 的注册方法仍是 draft。不要在文档里把它写成公开扩展契约。\n\n\n\n## AFD-I2 — 判断走控制面驱动还是 connector 驱动,只能看 `connector.control_plane` 是否为 `None`\n\n- 不能用 `isinstance` 或 connector 类名来选择执行路径(`CAP-INV-001`)。调用 `send_dp_metadata_list()`、`recv_dp_metadata_list()` 或 `update_state_from_dp_metadata()` 之前,必须先检查 `control_plane`。\n- GPU 只支持控制面驱动:`GPUFFNModelRunner` 构造时会断言 `control_plane` 不为 `None`;GPU daemon 遇到 `None` 会抛 `NotImplementedError`。所以任何新的 GPU connector 都必须在构造阶段装好 `AFDControlPlane`。\n- `control_plane is None` 的 connector(目前只有 `CAMAsyncAFDConnector`)不能进 FFN graph cache 路径,因为 cache key 依赖 DP metadata。这类 connector 必须保持 eager-only。Attention 在这种情况下会跳过 DP metadata 协调,所以路由和 token 元数据必须随 dispatch payload 一起发送。\n- 控制面对象属于 connector,没有独立的生命周期。不要给它单独加 close 或所有权逻辑。\n\n\n\n## AFD-I4 — 保持 CPU-safe import 边界:没有 CUDA、vLLM 或 torch_npu 时,默认测试也必须能跑\n\n- `vllm` 是可选 extra,默认的 `uv run pytest` 要能在没有 CUDA 的环境里运行。\n- 以下模块在 import 时不能引入 torch、vLLM 或 torch_npu:顶层包、通用配置和校验、版本检查、graph policy helper(`v1/worker/cuda_graph.py` 里的 `validate_cuda_graph_mode()`)。\n- `AFDConnectorFactory` 只保存懒加载器。新增 connector 时,不能在 factory 模块顶层 import CUDA 或 Ascend 的具体实现。\n- 只针对 CUDA 的插件注册路径不能 import vLLM-Ascend。Ascend 的 patch 必须等 Ascend 插件完成平台初始化之后再应用。\n- `afd_plugin._C_ascend` 只能在 connector 初始化时由 `ensure_cam_p2p_ops_available()` 懒加载。\n- CUDA 和 Ascend 的 runtime 模块可以直接 import 设备依赖。依赖 NPU 的测试属于 Ascend 环境的测试套件,不属于默认 CPU 测试。\n\n\n\n## AFD-I7 — `compute_gate_on_attention` 允许什么值,要同时看 connector、runner 版本和模型\n\n - `P2pNcclAFDConnector`(CUDA V1):`true` 和 `false` 都可以。\n - `CAMP2pAFDConnector`:通用字段和 `connector_extra_config` 里的同名字段都必须是 `false`。\n - `CAMAsyncAFDConnector`:文档里的配置要求通用字段为 `true`,开启 `async_moe_ubatching` 时也要求 `true`。\n - 任何 ModelRunnerV2 配对都要求 `false`。\n - Qwen3 MoE 和 Qwen3.5/Qwen3.6 MoE 都拒绝 `true`。\n- 修改其中一个维度的校验时,要核对另外两个维度,不能只在 connector parser 或只在模型层做校验。V2 的限制由 Attention 和 FFN 两个角色共用同一个校验器。\n- 通用字段 `compute_gate_on_attention` 是模型路由的选择开关;CAMP2P 的同名局部字段是算子契约。两者不能互相替代。\n- 同步更新 README 的模型表、connector 表和 Known gaps。\n\n\n\n## AFD-I8 — README 和设计文档对 ModelRunnerV2 的说法相互矛盾,改动 V2 时必须明确对齐\n\n - 设计文档和 E2E 契约描述了受支持的 CUDA V2(`AFDAttentionModelRunnerV2`),CI 在 `l4_4` 上按节点 ID 运行 `afd-v2-*-dp2` 和 `afd-v2-*-tp2` 四个用例。\n - Ascend V2 只有单元测试证据。\n- 涉及 V2 的 PR 要说明以哪份文档为准,并同步修改另一份。\n- 不能依据 README 删除 V2 代码路径或 CI 用例,也不能宣称 Ascend V2 经过了硬件验证。\n- V2 的约束由 Attention 和 FFN 共用同一个校验器,只改一端属于缺陷。约束包括:\n - `compute_gate_on_attention=false`;\n - PP、PCP、DCP 均为 1;\n - 角色 rank 数等于 DP x TP;\n - 静态 EP;\n - 不用 DBO 或 ubatching;\n- 图模式有平台差异:CUDA V2 只支持 eager 和 `FULL_DECODE_ONLY`,Ascend V2 还支持 `FULL`。不要把 Ascend 的 `FULL` 放宽到 CUDA。另外 README 的 connector 表把 CAMP2P 写成只支持 `FULL_DECODE_ONLY`,修改时要一并对齐。\n\n\n\n## AFD-I9 — AFD metadata 只能放在 `forward_context.additional_kwargs['afd_metadata']`\n\n- CUDA 和 Ascend、Attention 和 FFN、V1 和 V2,全部读写同一个键。不要重新引入 NPU 独立的 metadata 镜像,也不要新增其他 forward context 属性。\n- Ascend FFN 有两条路径:V1 用 `set_ascend_forward_context`;V2 配对用原生 `set_forward_context` 加 MRV2 profile override。修改 `compat/npu/forward_context.py` 时,两条路径都要测。\n- CUDA 空闲 DP rank 走的 dummy-batch 路径会绕过 `_model_forward()`,metadata 在这条路径上是惰性安装的。修改 metadata 的安装位置时,要检查这条路径。\n- `AFDForwardContextMetadata` 的结构和其中的 live connector 引用都不是稳定 API(#86、#88、#105 仍未解决)。不要在对外接口里承诺它们。\n\n\n\n## AFD-I10 — 修改控制面 payload 的字段,发送端、接收端和编解码都要同步\n\n- `AFDControlPayload` 只能携带插件自有的原始数据:整数 stage key、token count 列表、max count,以及 graph、warmup、profile 布尔标志。它经 JSON 编码后,先发长度再发 `uint8` tensor。不能序列化 vLLM 的 `DPMetadata` 或其他 vLLM 内部对象(`XFER-INV-001`)。\n- 新增字段时(`is_profile` 就是一个例子),以下内容都要一起更新:\n - `P2pNcclAFDControlPlane`(NCCL)和 `CAMP2pAFDControlPlane`(Gloo);\n - Attention 的所有发送点:非 ubatch、原生 ubatch、padded full-graph、V2 的 capture 和 replay hook;\n - FFN daemon 的 profile、warmup、capture、replay 分支;\n - 如果字段影响形状,还要改 `make_ffn_graph_key()` 或 ACL graph key。\n- 控制面必须先调用 `update_state_from_dp_metadata()` 更新本地 connector 状态,再发送。FFN 必须在进入 `torch.cuda.graph(...)` 或 `torch.npu.graph(...)` 之前完成状态更新,控制面的副作用不能被录进图里。\n\n\n\n## AFD-I11 — worker 自动选择和显式指定 worker 路径必须同时可用\n\n- `worker_cls=\"auto\"` 在上游平台规范化之后,由 `compat/patches/config_validation.py` 映射到 CUDA 或标准 Ascend 的角色 worker。显式指定的 worker 路径和非 AFD 配置不能被改写。\n- 显式 AFD worker 路径仍然接受(兼容旧命令),但不是稳定接口。\n- 不要删掉 NPU worker 里非 SP 场景的 all-to-all 修正。auto 路径在配置规范化时已经做了同样的改写,但显式指定 worker 的旧式启动还依赖 worker 里这份修正作为兜底。\n- `config_validation` 和其他核心 patch 在同一个 best-effort `try` 块里导入,失败时可能只在 debug 日志里出现,映射就静默丢了。因此 worker 初始化遇到隐式或错误的 worker class 时,必须明确报错,不能猜。\n- 没有 `--afd-config` 这个参数。配置只能从 `additional_config[\"afd\"]` 进入(`afd_role` 等兼容别名除外)。\n\n\n\n## AFD-I12 — E2E:分清 PR 门禁、每周门禁和只在本地跑的用例\n\n- PR 门禁用例最多使用 4 个设备,AFD 门禁用例必须是 2A2F。2A1F 用例(包括 README 本地冒烟示例里的 `afd-eager-2a1f`)不能加入 PR 门禁。\n - 一处说 2A1F 用例 “run outside CI”;\n - 另一处的每周门禁包含 DeepSeek `afd-graph-dbo-2a1f`,以及 Qwen3 MoE 和 Qwen3.6 的用例,其中 Qwen3.6 套件是 2A1F。\n - 修改 CI 选择时,要同步更新 `docs/design/module/e2e_testing.md` 和 `tests/e2e/README.md`,不要让这处矛盾继续扩大。\n- 按通用规则,harness 升级到 `SIGKILL` 就必须判用例失败。通过 `/proc/*/environ` 标记扫描再发 `SIGKILL` 的例外,只适用于 NPU async CAM 场景(`afd-eager-async-cam`、`afd-dsv4-flash-async-cam-dp2tp4-ep8`)。新场景不能沿用这个例外。\n- CI 不能设置 `AFD_GSM8K_LIMIT`,也不能调低 `AFD_GSM8K_THRESHOLD`。DBO 场景需要 12 路并发、至少 24 个样本,并断言至少记录到一次实际运行的双 ubatch 步骤。\n- 门禁用例不能用 `skip` 或 `xfail`。新行为需要单元测试加真实硬件证据;Ascend V2 目前只有单元测试证据,不能当作先例。\n\n\n\n## AFD-I3 — connector 构造只做轻量工作,跨角色的集合通信必须等权重加载完成后再做\n\n- 构造函数和 `parse_extra_config()` 里不能创建进程组、communicator、算子注册,也不能加载 CANN 算子。这些都属于 `init_afd_connector()`。\n- Attention 端:`init_afd_connector()` 必须放在 `load_model()` 末尾,并且在 ubatch wrapper 安装之后。\n- FFN 端:`init_afd_connector()` 必须在 `initialize_from_config()` 里调用,位置在空 KV-cache 初始化之后、daemon 线程启动之前。\n- 如果把 rendezvous 提前到 `init_device()` 或构造阶段,两个角色的权重加载就不能再重叠。无论怎么改,rendezvous 都必须在 Attention 显存 profiling 和首次 forward 之前完成。\n- 对未初始化或只初始化了一部分的 connector,`close()` 必须安全,并且之后仍能再次关闭。connector 的改动要附带清理测试,以及重试或重新初始化测试。\n- 缺少原生算子时,要在 connector 初始化时报错,不能在包 import 时报错。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-skeleton-AFD-I3", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I3: evidence swapped for AFD-I36's (another module)", + "rule_id": "AFD-I3", + "source": "synthetic", + "stage": "skeleton", + "synthetic": true, + "verdict": "pass" +} diff --git a/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I5.json b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I5.json new file mode 100644 index 00000000..7f558c0c --- /dev/null +++ b/adapters/afd_plugin/kb-calibration/cases/init-synthetic-sibling_evidence-skeleton-AFD-I5.json @@ -0,0 +1,64 @@ +{ + "base": { + "repos/afd-plugin/components/compatibility/rules.md": "---\ntitle: \"AFD compatibility 规则\"\ncreated: 2026-08-06\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin, components, compatibility, review]\nsources:\n - \"afd-plugin@a432692:AGENTS.md\"\n - \"afd-plugin@a432692:afd_plugin/compat/**\"\n - \"afd-plugin@a432692:docs/design/module/compatibility_and_patches.md\"\nconfidence: high\n---\n\n# AFD compatibility 规则\n\n这些规则从 [仓库规则](../../rules.md) 迁入最近 owner,保留原有稳定 ID。\n\n## AFD-3a — patch 必须 upstream-first 且可移除\n\n触发:新增 patch、修改已有 patch 或升级 vLLM/vLLM-Ascend。\n\n- 必须记录精确 upstream file/symbol/version/signature,优先复制对应版本函数,并保持签名和返回类型。\n- AFD 差异必须用 `# ### PATCH START: ...` / `# ### PATCH END: ...` 标出;函数上方必须说明 patch reason、行为变化和 upstream/移除条件。\n- 禁止在便利的全局 hook 中放置本应属于 AFD model、worker 或 connector 的功能。\n- 验收:AFD 和 non-AFD 分支、初始化/失败/shutdown 及需要时的 reload/idempotence 都有 focused tests,并写明移除路径。\n\n## AFD-3b — 上游 API drift 必须直接暴露\n\n触发:访问 vLLM/vLLM-Ascend 结构、添加 fallback 或选择 original-function delegation。\n\n- 必须直接访问预期上游函数和字段,让静态检查和原始错误暴露缺失/改名。\n- 禁止用宽泛 `getattr`、`hasattr`、`Any`、`object` 或 `_original_*` delegation 掩盖 drift。只有 `AGENTS.md` 明确允许的超大/不适合内联函数例外才可 delegation,且必须就地解释。\n- 验收:当前 pinned signature 对齐,故意缺失字段不会被 fallback 吞掉,保存的 original 不会在 reload 时被 wrapper 覆盖。\n\npatch 生命周期见 [architecture](architecture.md),版本支持声明见 [仓库规则](../../rules.md)。\n\n## AFD-I6 — 临时替换上游符号的代码必须在 `finally` 里还原,并登记到兼容文档\n\n- 这类临时替换包括:包装 `create_forward_context`、在 `Worker.init_device()` 期间替换 runner 符号、包装 V2 capture 的输入准备、安装 full-graph replay hook、给 DBO 临时关闭 metadata cache。它们都必须在 `finally` 里恢复符号和 runner 状态,异常路径也要恢复。\n- 需要有测试覆盖恢复逻辑和错误路径。参考 `test_attention_model_runner.py` 对 DBO 规避代码的覆盖。\n- 新增的这类替换要写进 `docs/design/module/compatibility_and_patches.md` 的 scoped adaptation 列表,否则升级审计会漏掉它。\n- 需要在整个进程生效的替换,应放到 `afd_plugin/compat/patches/`,并走 patch 审查流程。反过来,AFD 自己的功能不要借全局 hook 藏进 patch 模块。\n- 保存了原函数的 patch,在模块重载时不能用自己的 wrapper 覆盖已保存的原函数。\n\n\n" + }, + "evidence": [ + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L58-L58", + "text": "| `CAMP2pAFDConnector` | Ascend NPU | Decode | Sync | `FULL_DECODE_ONLY` ACL graph | Uses HCCL/CAMP2P custom ops. Ascend ops build by default on NPU platforms. See the [synchronous DeepSeek V3.2 recipe](recipe/npu/CAMP2pAFDConnector/deepseek_v3_2/README.md). |" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L68-L70", + "text": "- vLLM/vLLM-Ascend model runner v2 is not supported.\n- GPU and NPU E2E tests are opt-in and require real hardware plus model weights.\n- GPU CUDA graph support is limited to `FULL_DECODE_ONLY`." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:README.md:L180-L185", + "text": "GPU model runner v2 is not supported. Select model runner v1 before starting\neither GPU role:\n\n```bash\nexport VLLM_USE_V2_MODEL_RUNNER=0\n```" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/attention_runtime.md:L139-L162", + "text": "## ModelRunnerV2 boundary\n\nModelRunnerV2 is an upstream runtime selection, not an AFD configuration key.\nOn Attention, the V2 runners are thin subclasses of the native CUDA or Ascend\nrunner: native V2 retains request state, input preparation, Attention/KV\nexecution, sampling, and output ownership. `AFDMetadataProviderMixin` adds the\nconnector-facing sidecar, DP control payload, transaction IDs, and capture\nmetadata without porting V1 execution methods.\n\nThe supported V2 deployment is deliberately narrower than V1:\n\n| Constraint | CUDA V2 | Ascend V2 |\n| --- | --- | --- |\n| Connector | Synchronous `P2pNcclAFDConnector` | Synchronous `CAMP2pAFDConnector` |\n| Gate placement | `compute_gate_on_attention=false` | `compute_gate_on_attention=false` |\n| Parallelism | PP=PCP=DCP=1; configured role ranks equal DP x TP; static EP enabled | Same |\n| Excluded features | Elastic EP, EPLB, sequence-parallel MoE, compile SP, DBO, and ubatching | Same |\n| Graph execution | Eager or `FULL_DECODE_ONLY` | Eager, `FULL`, or `FULL_DECODE_ONLY` |\n| Model | Must resolve to a registered AFD architecture | Same |\n\nBoth roles validate the paired V2 topology, but only Attention uses the native\nV2 model runner. FFN remains connector-driven and uses its existing AFD runner\nsurface. CUDA V2 has DP2/TP2 eager and graph E2E evidence; the current Ascend\nV2 contract has focused unit evidence but no repository hardware E2E case." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/ffn_runtime.md:L129-L148", + "text": "## ModelRunnerV2 pairing\n\nFFN remains connector-driven when the upstream configuration selects\nModelRunnerV2. It does not adopt native V2 request state or scheduler-driven\nexecution:\n\n- CUDA temporarily substitutes `GPUFFNModelRunner` at the native V2\n construction seam inside `Worker.init_device()`; the existing AFD runner is\n still the runtime object.\n- Ascend always constructs `AFDNPUFFNModelRunner`. When V2 is selected, its\n connector-driven forward enters the upstream MRV2 forward-context shape so\n Ascend-specific state is installed in `additional_kwargs`.\n- Both workers run the same V2 topology and feature validator as the paired\n Attention role before constructing communication resources.\n\nThe V2 pair therefore requires the synchronous platform connector,\n`compute_gate_on_attention=false`, PP/PCP/DCP size 1, configured role ranks\nequal to DP x TP, static EP, a registered AFD model, and no DBO or ubatching.\nCUDA uses `P2pNcclAFDConnector`; Ascend uses `CAMP2pAFDConnector`. These limits\ndescribe the pair even though only Attention inherits a native V2 runner." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/e2e_testing.md:L105-L110", + "text": "| `afd-v2-eager-dp2` | CUDA ModelRunnerV2 eager, Attention DP2 + FFN DP2 | 4 | V2 data-parallel lifecycle and accuracy. |\n| `afd-v2-graph-dp2` | CUDA ModelRunnerV2 graph, Attention DP2 + FFN DP2 | 4 | V2 data-parallel full-decode graph path. |\n| `afd-v2-eager-tp2` | CUDA ModelRunnerV2 eager, Attention TP2 + FFN TP2 | 4 | V2 tensor-parallel lifecycle and accuracy. |\n| `afd-v2-graph-tp2` | CUDA ModelRunnerV2 graph, Attention TP2 + FFN TP2 | 4 | V2 tensor-parallel full-decode graph path. |\n\nCUDA CI runs the legacy and ModelRunnerV2 matrices as separate jobs with a" + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/e2e_testing.md:L147-L151", + "text": "The ModelRunnerV2 matrix is CUDA-only in the current E2E harness. Its local\n1A1F cases are `afd-v2-eager-1a1f` and `afd-v2-graph-1a1f`; CI selects the four\nDP2/TP2 cases listed above by exact node ID on `l4_4`. The Ascend\nModelRunnerV2 implementation currently has focused unit evidence but no E2E\ncase, so it is not included in the hardware gate." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L281-L285", + "text": "The NPU V2 runner supports eager, `FULL`, and `FULL_DECODE_ONLY`. Like CUDA V2,\nit publishes descriptor-matched warmup/capture control outside formal graph\ncapture and installs an instance-scoped pre-replay hook because native full\nreplay creates no `ForwardContext`. V2 does not use `AscendUBatchWrapper` and\nrejects DBO/ubatching." + }, + { + "kind": "upstream_text", + "source_reference": "vllm-project/afd-plugin@9cae2d2ddaae:docs/design/module/execution_platforms.md:L341-L348", + "text": "| CUDA V2 + `P2pNcclAFDConnector` | Eager or `FULL_DECODE_ONLY` native V2 CUDA Graph | DBO and ubatching rejected | `compute_gate_on_attention=false`; PP/CP, elastic EP, EPLB, SP MoE, and compile SP rejected; role ranks equal DP x TP | DeepSeek-V2-Lite eager/graph DP2 and TP2 accuracy E2E, plus focused V2 unit tests |\n| Ascend V1 + `CAMP2pAFDConnector` | Eager or current ACL Graph path | Native DBO, exactly two ubatches | Common and connector-local `compute_gate_on_attention=false`; `connector_extra_config.quant_mode=0`; plugin CANN ops required | Backend-neutral DeepSeek-V2-Lite eager/graph/DBO accuracy cases plus NPU runtime, graph, ops, connector, and profiler unit tests |\n| Ascend V2 + `CAMP2pAFDConnector` | Eager, `FULL`, or `FULL_DECODE_ONLY` native V2 ACL Graph | DBO and ubatching rejected | `compute_gate_on_attention=false`; PP/CP, elastic EP, EPLB, SP MoE, and compile SP rejected; role ranks equal DP x TP | Focused runner, context, validation, and device-contract unit tests; no repository hardware E2E case |\n| Ascend + `CAMAsyncAFDConnector` | Eager only | Native DBO rejected; optional AFD-managed MoE ubatching uses exactly two request or token-balanced stages | Experimental v0.26 port; `async=true`; documented path uses common `compute_gate_on_attention=true`; token mode requires Attention TP > 1; model runner v1 PCP is unsupported; prefill and decode context parallelism are unsupported; `connector_extra_config.dynamicQuant` is 0 or 1; plugin-owned async CAM ops required | Focused unit coverage; pre-fix DP3TP2/EP2 six-case E2E matrix; post-fix full 61-layer DP2TP8+EP16 token-split run reached `0.9522` strict match on the complete GSM8K evaluation |\n\nAll rows target vLLM 0.26.0. Hardware validation exists for CUDA V1/V2 and the\nrecorded Ascend V1 paths; the Ascend V2 row is an implemented, unit-tested\ncontract rather than a hardware-validated claim. GPU/NPU rank topology and" + } + ], + "expected": "reject", + "head": { + "repos/afd-plugin/components/compatibility/rules.md": "---\ntitle: \"AFD compatibility 规则\"\ncreated: 2026-08-06\nupdated: 2026-09-30\ntype: rule\ntags: [afd-plugin, components, compatibility, review]\nsources:\n - \"afd-plugin@a432692:AGENTS.md\"\n - \"afd-plugin@a432692:afd_plugin/compat/**\"\n - \"afd-plugin@a432692:docs/design/module/compatibility_and_patches.md\"\nconfidence: high\n---\n\n# AFD compatibility 规则\n\n这些规则从 [仓库规则](../../rules.md) 迁入最近 owner,保留原有稳定 ID。\n\n## AFD-3a — patch 必须 upstream-first 且可移除\n\n触发:新增 patch、修改已有 patch 或升级 vLLM/vLLM-Ascend。\n\n- 必须记录精确 upstream file/symbol/version/signature,优先复制对应版本函数,并保持签名和返回类型。\n- AFD 差异必须用 `# ### PATCH START: ...` / `# ### PATCH END: ...` 标出;函数上方必须说明 patch reason、行为变化和 upstream/移除条件。\n- 禁止在便利的全局 hook 中放置本应属于 AFD model、worker 或 connector 的功能。\n- 验收:AFD 和 non-AFD 分支、初始化/失败/shutdown 及需要时的 reload/idempotence 都有 focused tests,并写明移除路径。\n\n## AFD-3b — 上游 API drift 必须直接暴露\n\n触发:访问 vLLM/vLLM-Ascend 结构、添加 fallback 或选择 original-function delegation。\n\n- 必须直接访问预期上游函数和字段,让静态检查和原始错误暴露缺失/改名。\n- 禁止用宽泛 `getattr`、`hasattr`、`Any`、`object` 或 `_original_*` delegation 掩盖 drift。只有 `AGENTS.md` 明确允许的超大/不适合内联函数例外才可 delegation,且必须就地解释。\n- 验收:当前 pinned signature 对齐,故意缺失字段不会被 fallback 吞掉,保存的 original 不会在 reload 时被 wrapper 覆盖。\n\npatch 生命周期见 [architecture](architecture.md),版本支持声明见 [仓库规则](../../rules.md)。\n\n## AFD-I6 — 临时替换上游符号的代码必须在 `finally` 里还原,并登记到兼容文档\n\n- 这类临时替换包括:包装 `create_forward_context`、在 `Worker.init_device()` 期间替换 runner 符号、包装 V2 capture 的输入准备、安装 full-graph replay hook、给 DBO 临时关闭 metadata cache。它们都必须在 `finally` 里恢复符号和 runner 状态,异常路径也要恢复。\n- 需要有测试覆盖恢复逻辑和错误路径。参考 `test_attention_model_runner.py` 对 DBO 规避代码的覆盖。\n- 新增的这类替换要写进 `docs/design/module/compatibility_and_patches.md` 的 scoped adaptation 列表,否则升级审计会漏掉它。\n- 需要在整个进程生效的替换,应放到 `afd_plugin/compat/patches/`,并走 patch 审查流程。反过来,AFD 自己的功能不要借全局 hook 藏进 patch 模块。\n- 保存了原函数的 patch,在模块重载时不能用自己的 wrapper 覆盖已保存的原函数。\n\n\n\n## AFD-I5 — 升级 vLLM 或 vLLM-Ascend 基线时,需要同步修改多个文件并做完整审计\n\n- vLLM 目标版本出现在三个地方:`pyproject.toml` 的 extra pin、`afd_plugin/compat/vllm.py` 的版本检查、README(Overview、Known gaps、GPU installation)。三处必须一致。\n- 插件注册用 `strict=False` 调版本检查,版本不符只告警。不能把“注册成功”当作新版本受支持的证据(`PATCH-INV-003`)。\n - `engine_core.py` 和 `npu/force_load_balance.py` 没有 patch 内的版本守卫,只能靠 review 把关。\n - `async_dp_engine.py` 里复制的 coordinator 只在 `0.26.0` 精确版本下生效,守卫要跟着更新。\n - `attention_model_runner.py` 里规避 DBO metadata cache 的代码:等 vLLM #48659 进入 pin 的版本后删除。\n - 连接器进程组用到的私有 PyTorch 和 vLLM 符号。\n- `pyproject.toml` 没有声明 vLLM-Ascend 依赖。刷新 Ascend 侧时,必须引用确切的 vLLM-Ascend 源码 commit(当前是 `80d8c194f`),同步 README 的 Ascend 基线表,并附上 NPU 单元测试和 E2E 证据。\n\n\n" + }, + "id": "init-synthetic-sibling_evidence-skeleton-AFD-I5", + "mutation": "sibling_evidence", + "note": "kb init synthetic bad case from good rule AFD-I5: evidence swapped for AFD-I8's (another module)", + "rule_id": "AFD-I5", + "source": "synthetic", + "stage": "skeleton", + "synthetic": true, + "verdict": "pass" +} diff --git a/adapters/afd_plugin/manifest.yaml b/adapters/afd_plugin/manifest.yaml index d3abf153..8d14e6e5 100644 --- a/adapters/afd_plugin/manifest.yaml +++ b/adapters/afd_plugin/manifest.yaml @@ -266,3 +266,4 @@ knowledge_lifecycle: # does not read (its knowledge stays with the execution-platforms owner) source_roots: [afd_plugin/] exclude: ["*/__init__.py", "*/py.typed"] + calibration_set: kb-calibration