Skip to content

Commit 30a9d98

Browse files
committed
fix(ascend): repair Qwen3.6 accuracy on Ascend (attention/moe/gdn)
修复 Qwen3.6 在 Ascend 上的精度问题(注意力/MoE/GDN 四项) 1. attention: drop the mathematically invalid head-split fallback for head_size=256 prefill and call npu_fused_infer_attention_score (TND) directly, matching vllm-ascend. Splitting a 256-dim head into two independent 128-dim softmax attentions is not an identity transform (measured max err 0.0076 direct vs 4.30 head-split on NPU). 注意力:删除 head_dim=256 时数学上不成立的拆头回退路径,prefill 直连 FIA(TND),与 vllm-ascend 一致。将 256 维头拆成两个独立 128 维 softmax 注意力并非恒等变换(NPU 实测直连误差 0.0076,旧路径 4.30)。 2. attention: derive attn_state from num_computed_tokens; use PrefillNoCache only when every request starts at token 0. Pure-prefill batches containing prefix-cache hits or chunked-prefill continuations previously dropped all cached KV (few-shot context lost). 注意力:attn_state 依据 num_computed_tokens 判定,仅当所有请求从第 0 个 token 开始才走 PrefillNoCache。此前含 prefix 命中或续写 chunk 的 纯 prefill batch 会丢弃全部已缓存 KV(few-shot 上下文丢失)。 3. moe: cast router probs to hidden dtype (bf16) before npu_moe_token_unpermute, matching vllm-ascend; the op expects bf16 probs. MoE:npu_moe_token_unpermute 前将路由 probs 转为 hidden dtype (bf16),与 vllm-ascend 一致;该算子 probs 期望 bf16。 4. gdn: default VLLM_FL_DISABLE_PTO_GDN to 1. The PTO megakernel carries A/A_inv/recurrent state in fp16, while the Triton chunk path keeps fp32 state; the fp16 pipeline measurably degrades accuracy. Re-enable after reworking the kernel state path to fp32. GDN:VLLM_FL_DISABLE_PTO_GDN 默认改为 1。PTO megakernel 的 A/A_inv/循环状态均为 fp16,而 Triton chunk 路径保持 fp32 状态,fp16 链路实测显著拉低精度。待 kernel 状态链路 fp32 化后可重新开启。 Verified on 910B: op-level tests for FIA head256 / chunked-prefill FIA / recurrent GDN decode / causal-conv1d decode all pass; gsm8k Qwen3.6-27B (128 samples) recovers to 0.9453 (previously in the 0.47-0.88 range). 验证:FIA head256 / chunked prefill FIA / GDN recurrent decode / causal-conv1d decode 算子级测试全部通过;gsm8k Qwen3.6-27B(128 样本) 恢复至 0.9453(此前在 0.47-0.88 区间)。
1 parent 27cc2ff commit 30a9d98

3 files changed

Lines changed: 22 additions & 146 deletions

File tree

vllm_fl/dispatch/backends/vendor/ascend/impl/attention.py

Lines changed: 13 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,6 @@
5353

5454
logger = logging.getLogger(__name__)
5555

56-
# Ascend npu_fused_infer_attention_score with input_layout="TND" only supports
57-
# head_dim in {64, 128, 192} (plus the special qD=kD=192, vD=128 case).
58-
_PFA_TND_SUPPORTED_HEAD_DIMS = frozenset({64, 128, 192})
59-
6056
# Check torch_npu availability and setup NPU compatibility
6157
_TORCH_NPU_AVAILABLE = False
6258
try:
@@ -324,7 +320,9 @@ def build(
324320

325321
# Determine attention state
326322
attn_state = self._determine_attn_state(
327-
num_decodes, num_prefills, num_decode_tokens, num_prefill_tokens
323+
num_decodes, num_prefills, num_decode_tokens, num_prefill_tokens,
324+
num_computed_tokens_cpu=getattr(
325+
common_attn_metadata, 'num_computed_tokens_cpu', None),
328326
)
329327

330328
# Create attention mask based on state
@@ -368,13 +366,21 @@ def _determine_attn_state(
368366
num_prefills: int,
369367
num_decode_tokens: int,
370368
num_prefill_tokens: int,
369+
num_computed_tokens_cpu: Optional[torch.Tensor] = None,
371370
) -> AscendAttentionState:
372371
"""Determine attention state based on batch composition."""
373372
if num_prefills == 0:
374373
return AscendAttentionState.DecodeOnly
374+
# PrefillNoCache is only valid when every request starts from token 0.
375+
# A pure-prefill batch may still contain prefix-cache hits or chunked
376+
# prefill continuations; those must attend to the paged KV cache via
377+
# the ChunkedPrefill path (same handling as vllm-ascend).
378+
if (num_computed_tokens_cpu is not None
379+
and len(num_computed_tokens_cpu) > 0
380+
and int(num_computed_tokens_cpu.max()) > 0):
381+
return AscendAttentionState.ChunkedPrefill
375382
elif num_decodes == 0 and num_prefill_tokens > 0:
376-
# Pure prefill - check if cache hit or no cache
377-
# For simplicity, use ChunkedPrefill as default
383+
# Fresh prefill with no cached context
378384
return AscendAttentionState.PrefillNoCache
379385
else:
380386
# Mixed decode and prefill
@@ -563,13 +569,6 @@ def __init__(
563569
self.num_queries_per_kv = self.num_heads // self.num_kv_heads
564570
self.key_cache = None
565571
self.value_cache = None
566-
self._use_fusion_fallback = self.head_size not in _PFA_TND_SUPPORTED_HEAD_DIMS
567-
if self._use_fusion_fallback:
568-
logger.info(
569-
"AscendAttentionBackendImpl: head_size=%d is not supported by "
570-
"npu_fused_infer_attention_score TND layout, falling back to "
571-
"head-split attention (2x%d).", self.head_size,
572-
self.head_size // 2)
573572

574573
@classmethod
575574
def update_graph_params(
@@ -707,130 +706,6 @@ def reshape_and_cache(
707706
)
708707
return key, value
709708

710-
def _gather_kv_cache(
711-
self,
712-
attn_metadata: AscendMetadata,
713-
) -> Tuple[torch.Tensor, torch.Tensor]:
714-
"""Gather paged KV cache into dense (T_kv, num_kv_heads, head_size).
715-
716-
Only the blocks actually referenced by each request's sequence length
717-
are gathered. The block table is padded to ``max_num_blocks_per_req``,
718-
so gathering the whole table would allocate ``batch_size`` times more
719-
memory than necessary and can OOM on long-context configs.
720-
"""
721-
if self.key_cache is None or self.value_cache is None:
722-
raise RuntimeError(
723-
"key_cache/value_cache is not initialized for KV gather")
724-
725-
seq_lens = attn_metadata.seq_lens_list
726-
batch_size = len(seq_lens)
727-
block_size = self.key_cache.shape[1]
728-
block_table = attn_metadata.block_tables[:batch_size]
729-
730-
k_list: List[torch.Tensor] = []
731-
v_list: List[torch.Tensor] = []
732-
for i, sl in enumerate(seq_lens):
733-
num_blocks = (sl + block_size - 1) // block_size
734-
block_ids = block_table[i, :num_blocks].long()
735-
gathered_k = self.key_cache[block_ids].view(
736-
-1, self.num_kv_heads, self.head_size)[:sl]
737-
gathered_v = self.value_cache[block_ids].view(
738-
-1, self.num_kv_heads, self.head_size)[:sl]
739-
k_list.append(gathered_k)
740-
v_list.append(gathered_v)
741-
742-
return torch.cat(k_list, dim=0), torch.cat(v_list, dim=0)
743-
744-
def _split_heads_for_pfa(
745-
self,
746-
x: torch.Tensor,
747-
num_heads: int,
748-
) -> torch.Tensor:
749-
"""Split head dimension to a supported PFA TND head size.
750-
751-
Ascend's npu_fused_infer_attention_score TND layout only supports
752-
head_dim in {64, 128, 192}. For unsupported head sizes such as 256,
753-
we split each head into two heads of half the dimension and double the
754-
head count, which is mathematically equivalent for self-attention.
755-
756-
Accepts either 2D input [T, num_heads * head_size] or 3D input
757-
[T, num_heads, head_size]. Output shape: [T, num_heads * 2,
758-
head_size // 2].
759-
"""
760-
head_size_128 = self.head_size // 2
761-
if x.dim() == 2:
762-
x = x.view(-1, num_heads, self.head_size)
763-
return x.view(-1, num_heads, 2, head_size_128).reshape(
764-
-1, num_heads * 2, head_size_128)
765-
766-
def _merge_heads_from_pfa(
767-
self,
768-
x: torch.Tensor,
769-
) -> torch.Tensor:
770-
"""Merge split heads back to original head dimension.
771-
772-
Input shape: [T, num_heads * 2, head_size // 2]
773-
Output shape: [T, num_heads * head_size]
774-
"""
775-
head_size_128 = self.head_size // 2
776-
return x.view(-1, self.num_heads, 2, head_size_128).reshape(
777-
-1, self.num_heads * self.head_size)
778-
779-
def _forward_fusion_attention(
780-
self,
781-
query: torch.Tensor,
782-
key: torch.Tensor,
783-
value: torch.Tensor,
784-
attn_metadata: AscendMetadata,
785-
output: torch.Tensor,
786-
) -> torch.Tensor:
787-
"""Forward pass for unsupported head sizes using head-split PFA.
788-
789-
Splits head_dim into two supported heads and runs
790-
npu_fused_infer_attention_score (PFA) with the standard TND path.
791-
This keeps the implementation compatible with ACL graph capture while
792-
avoiding the per-batch dense causal mask limitation of
793-
npu_fusion_attention.
794-
"""
795-
num_tokens = attn_metadata.actual_seq_lengths_q[-1]
796-
query = query[:num_tokens]
797-
798-
if attn_metadata.attn_state == AscendAttentionState.PrefillNoCache:
799-
key = key[:num_tokens]
800-
value = value[:num_tokens]
801-
actual_seq_kvlen = attn_metadata.actual_seq_lengths_q
802-
else:
803-
key, value = self._gather_kv_cache(attn_metadata)
804-
actual_seq_kvlen = [
805-
sum(attn_metadata.seq_lens_list[:i + 1])
806-
for i in range(len(attn_metadata.seq_lens_list))
807-
]
808-
809-
query = self._split_heads_for_pfa(query, self.num_heads)
810-
key = self._split_heads_for_pfa(key, self.num_kv_heads)
811-
value = self._split_heads_for_pfa(value, self.num_kv_heads)
812-
813-
attn_output, _ = torch_npu.npu_fused_infer_attention_score(
814-
query=query,
815-
key=key,
816-
value=value,
817-
atten_mask=attn_metadata.attn_mask,
818-
block_table=None,
819-
input_layout="TND",
820-
block_size=AscendAttentionBackend.get_supported_block_size()[0],
821-
actual_seq_lengths=attn_metadata.actual_seq_lengths_q,
822-
actual_seq_lengths_kv=actual_seq_kvlen,
823-
num_key_value_heads=self.num_kv_heads * 2,
824-
num_heads=self.num_heads * 2,
825-
scale=self.scale,
826-
sparse_mode=3,
827-
)
828-
829-
attn_output = self._merge_heads_from_pfa(attn_output)
830-
attn_output = attn_output.view(num_tokens, self.num_heads, self.head_size)
831-
output[:num_tokens] = attn_output[:num_tokens]
832-
return output
833-
834709
def forward_fused_infer_attention(
835710
self,
836711
query: torch.Tensor,
@@ -840,11 +715,6 @@ def forward_fused_infer_attention(
840715
output: torch.Tensor,
841716
) -> torch.Tensor:
842717
"""Forward pass using fused_infer_attention_score."""
843-
if (self._use_fusion_fallback
844-
and attn_metadata.attn_state != AscendAttentionState.DecodeOnly):
845-
return self._forward_fusion_attention(
846-
query, key, value, attn_metadata, output)
847-
848718
key, value, block_size, block_table, actual_seq_lengths_kv = \
849719
self._get_fia_params(key, value, attn_metadata)
850720

vllm_fl/dispatch/backends/vendor/ascend/impl/fused_moe.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,11 @@ def _ascendc_fused_experts_impl(
228228
)[0]
229229

230230
# Scatter/sum the expert outputs back to the token dimension.
231+
# Match vllm-ascend: npu_moe_token_unpermute expects bf16 probs.
231232
out = torch_npu.npu_moe_token_unpermute(
232233
permuted_tokens=down,
233234
sorted_indices=expanded_row_idx.abs(),
234-
probs=probs,
235+
probs=probs.to(down.dtype) if probs is not None else None,
235236
)
236237

237238
if inplace:
@@ -357,10 +358,11 @@ def _torch_fused_experts_impl(
357358
)[0]
358359

359360
# Scatter/sum the expert outputs back to the token dimension.
361+
# Match vllm-ascend: npu_moe_token_unpermute expects bf16 probs.
360362
out = torch_npu.npu_moe_token_unpermute(
361363
permuted_tokens=down,
362364
sorted_indices=sorted_indices,
363-
probs=probs,
365+
probs=probs.to(down.dtype) if probs is not None else None,
364366
)
365367

366368
if inplace:

vllm_fl/dispatch/backends/vendor/ascend/patches/patch_qwen3_6_gdn.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,11 @@ def _pto_available() -> bool:
274274
global _PTO_AVAILABLE
275275
if _PTO_AVAILABLE is not None:
276276
return _PTO_AVAILABLE
277-
if os.environ.get("VLLM_FL_DISABLE_PTO_GDN", "0") == "1":
277+
# Default OFF: the PTO megakernel carries A/A_inv/recurrent state in fp16
278+
# (see vllm_fl/ops/pto_chunk_gdn/mega_kernel.py), which measurably degrades
279+
# accuracy vs the fp32-state Triton chunk path. Re-enable with
280+
# VLLM_FL_DISABLE_PTO_GDN=0 once the kernel is reworked to fp32 state.
281+
if os.environ.get("VLLM_FL_DISABLE_PTO_GDN", "1") == "1":
278282
logger.info("VLLM_FL_DISABLE_PTO_GDN=1, keep Triton chunk path")
279283
_PTO_AVAILABLE = False
280284
return False

0 commit comments

Comments
 (0)