Skip to content

Commit 711887e

Browse files
authored
[Ascend] Integrate transformer_engine_npu && Fix the GEMM operator bug in the reference backend (#89)
## Summary This PR adds Ascend NPU support to the TE-FL plugin system through `torch_npu` and `transformer_engine_npu`, and fixes backward-path issues in the reference GEMM implementation. ## Changes ### Ascend NPU backend - Add automatic NPU availability detection and vendor-priority registration. - Add support for: - FlashAttention with SBHD, BSHD, and THD layouts - RMSNorm forward and backward - Generic and grouped GEMM - Multi-tensor scale and L2-norm operations - Add THD ↔ BSHD conversion operators. - Keep NPU dependencies lazily imported. ### Reference GEMM fixes - Fix output shape restoration for transposed inputs. - Do not add forward bias in backward mode. - Compute fused bias gradients. - Apply dGeLU using the saved forward activation. - Preserve correct alpha scaling and 3D input behavior. ## Testing Added coverage for: - FlashAttention forward/backward accuracy and causal masking - RMSNorm forward/backward - Generic and grouped GEMM - Multi-tensor and FP8 scale operations - Reference GEMM backward behavior Verified on Ascend 910C: ```text 46 passed ``` ## Deps It depends on TransformerEngineNPU. The package natively generated by TransformerEngineNPU is named transformer_engine. Relevant packaging logic needs to be modified so that the generated package is named transformer_engine_npu.
1 parent 2cb485f commit 711887e

10 files changed

Lines changed: 2633 additions & 10 deletions

File tree

transformer_engine/plugin/core/backends/reference/impl/gemm.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from typing import Any, Optional, Tuple, Union
66
import torch
77

8+
from .activation import dgelu_torch
9+
810
__all__ = [
911
"general_gemm_torch",
1012
]
@@ -84,25 +86,33 @@ def general_gemm_torch(
8486
if alpha != 1.0:
8587
out = out * alpha
8688

87-
if original_B_shape is not None:
89+
# A non-transposed B contributes its outer dimensions to the output. A
90+
# transposed B does not, so its flattened shape must not be restored (the
91+
# latter is the layout normally used by weight-gradient GEMMs).
92+
if original_B_shape is not None and not transB:
8893
out = out.view(original_B_shape[0], original_B_shape[1], -1)
8994

9095
gelu_input_ret = None
91-
if gelu and gelu_in is not None:
92-
pass
9396

94-
if bias is not None:
97+
# In a backward GEMM, `bias` only requests the fused BGRAD epilogue. Its
98+
# value is not added to the GEMM result.
99+
if bias is not None and not grad:
95100
if bias.device != target_device:
96101
bias = bias.to(target_device)
97102
out = out + bias
98103

99104
if gelu:
100-
if gelu_in is not None:
101-
gelu_in.copy_(out)
102-
gelu_input_ret = gelu_in
105+
if grad:
106+
if gelu_in is None:
107+
raise ValueError("gelu_in must be provided for a backward GELU GEMM")
108+
out = dgelu_torch(out, gelu_in, quantizer=None)
103109
else:
104-
gelu_input_ret = out.clone()
105-
out = F.gelu(out, approximate="tanh")
110+
if gelu_in is not None:
111+
gelu_in.copy_(out)
112+
gelu_input_ret = gelu_in
113+
else:
114+
gelu_input_ret = out.clone()
115+
out = F.gelu(out, approximate="tanh")
106116

107117
torch_out_dtype = _convert_dtype(output_dtype)
108118
if torch_out_dtype is not None and out.dtype != torch_out_dtype:
@@ -121,7 +131,9 @@ def general_gemm_torch(
121131

122132
bias_grad = None
123133
if grad and bias is not None:
124-
pass
134+
# cuBLASLt's BGRADB epilogue always reduces GEMM input B. Flattening
135+
# all leading dimensions also handles sequence-shaped gradient input.
136+
bias_grad = B.sum(dim=0).to(dtype=out.dtype)
125137

126138
extra_output_ret = None
127139

transformer_engine/plugin/core/backends/reference/reference.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,52 @@ def multi_tensor_compute_scale_inv_e8m0(
740740
block_len,
741741
)
742742

743+
def convert_thd_to_bshd(
744+
self,
745+
tensor: torch.Tensor,
746+
cu_seqlens: torch.Tensor,
747+
b: int,
748+
max_seq_len: int,
749+
) -> torch.Tensor:
750+
"""Convert THD (packed tokens) format to BSHD (batched, padded) format."""
751+
# tensor shape: [total_tokens, num_heads, head_dim]
752+
# output shape: [b, max_seq_len, num_heads, head_dim]
753+
remaining_dims = tensor.shape[1:]
754+
output = torch.zeros(
755+
(b, max_seq_len) + remaining_dims,
756+
dtype=tensor.dtype,
757+
device=tensor.device,
758+
)
759+
for i in range(b):
760+
start = cu_seqlens[i].item()
761+
end = cu_seqlens[i + 1].item()
762+
seq_len = end - start
763+
output[i, :seq_len] = tensor[start:end]
764+
return output
765+
766+
def convert_bshd_to_thd(
767+
self,
768+
tensor: torch.Tensor,
769+
cu_seqlens: torch.Tensor,
770+
t: int,
771+
) -> torch.Tensor:
772+
"""Convert BSHD (batched, padded) format to THD (packed tokens) format."""
773+
# tensor shape: [b, max_seq_len, num_heads, head_dim]
774+
# output shape: [t, num_heads, head_dim]
775+
b = tensor.shape[0]
776+
remaining_dims = tensor.shape[2:]
777+
output = torch.zeros(
778+
(t,) + remaining_dims,
779+
dtype=tensor.dtype,
780+
device=tensor.device,
781+
)
782+
for i in range(b):
783+
start = cu_seqlens[i].item()
784+
end = cu_seqlens[i + 1].item()
785+
seq_len = end - start
786+
output[start:end] = tensor[i, :seq_len]
787+
return output
788+
743789
def get_flash_attention_class(self):
744790
from .flash_attention import FlashAttentionTorch
745791

transformer_engine/plugin/core/backends/reference/register_ops.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,23 @@ def register_builtins(registry) -> None:
516516
vendor=None,
517517
priority=50,
518518
),
519+
# THD <-> BSHD format conversion
520+
OpImpl(
521+
op_name="convert_thd_to_bshd",
522+
impl_id="reference.torch",
523+
kind=BackendImplKind.REFERENCE,
524+
fn=_bind_is_available(backend.convert_thd_to_bshd, is_avail),
525+
vendor=None,
526+
priority=50,
527+
),
528+
OpImpl(
529+
op_name="convert_bshd_to_thd",
530+
impl_id="reference.torch",
531+
kind=BackendImplKind.REFERENCE,
532+
fn=_bind_is_available(backend.convert_bshd_to_thd, is_avail),
533+
vendor=None,
534+
priority=50,
535+
),
519536
# FlashAttention class getter
520537
OpImpl(
521538
op_name="get_flash_attention_class",
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Copyright (c) 2025, BAAI. All rights reserved.
2+
# Copyright (c) 2026, Huawei Technologies Co., Ltd. All rights reserved.
3+
#
4+
# See LICENSE for license information.
5+
6+
from .npu import NPUBackend
7+
8+
__all__ = ["NPUBackend"]
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
# Copyright (c) 2026, BAAI. All rights reserved.
2+
# Copyright (c) 2026, Huawei Technologies Co., Ltd. All rights reserved.
3+
#
4+
# See LICENSE for license information.
5+
6+
"""NPU Flash Attention adapter.
7+
8+
Bridges TE-FL's FlashAttention calling convention to NPU's npu_fusion_attention kernel.
9+
10+
TE-FL passes many parameters (qkv_layout, window_size, cp_group, fp8, etc.)
11+
that NPU's FlashAttention doesn't support. This adapter:
12+
1. Accepts the full TE-FL parameter set
13+
2. Maps qkv_layout → qkv_format (sbhd/thd)
14+
3. Forwards only the supported parameters to NPU's FlashAttention
15+
4. Silently ignores unsupported features (sliding window, CP, FP8, ALiBi)
16+
"""
17+
18+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
19+
20+
import torch
21+
22+
from transformer_engine.plugin.core.ops import FlashAttentionBase
23+
24+
25+
_COMPRESSED_MASK_SIZE = 2048
26+
27+
_COMPRESSED_CAUSAL_MASK = None
28+
29+
30+
def get_compressed_causal_mask(device="npu"):
31+
global _COMPRESSED_CAUSAL_MASK
32+
if _COMPRESSED_CAUSAL_MASK is None:
33+
_COMPRESSED_CAUSAL_MASK = torch.triu(
34+
torch.ones(
35+
(_COMPRESSED_MASK_SIZE, _COMPRESSED_MASK_SIZE),
36+
device=device,
37+
dtype=torch.bool,
38+
),
39+
diagonal=1,
40+
)
41+
return _COMPRESSED_CAUSAL_MASK
42+
43+
44+
class NPUFlashAttention(FlashAttentionBase):
45+
"""FlashAttention adapter for NPU (Ascend) hardware.
46+
47+
Wraps transformer_engine_npu's FlashAttention, which calls
48+
torch_npu.npu_fusion_attention under the hood.
49+
50+
Supported features:
51+
- sbhd, bshd (via transpose), and thd formats
52+
- causal / padding mask types
53+
- Variable-length sequences (cu_seqlens)
54+
- Sparse mask optimization (via NPU's get_fa_config)
55+
56+
Not supported (silently ignored):
57+
- Sliding window attention (window_size)
58+
- ALiBi slopes
59+
- Context Parallelism (cp_group, cp_stream, etc.)
60+
- FP8 / quantization
61+
- KV cache (inference_params)
62+
- FA v2/v3 version selection
63+
"""
64+
65+
def __init__(
66+
self,
67+
softmax_scale: float,
68+
attention_dropout: float = 0.0,
69+
attention_dropout_ctx: Optional[Callable] = None,
70+
attention_type: str = "self",
71+
layer_number: Optional[int] = None,
72+
deterministic: bool = False,
73+
**kwargs,
74+
) -> None:
75+
super().__init__(
76+
softmax_scale=softmax_scale,
77+
attention_dropout=attention_dropout,
78+
attention_dropout_ctx=attention_dropout_ctx,
79+
attention_type=attention_type,
80+
layer_number=layer_number,
81+
deterministic=deterministic,
82+
)
83+
self.softmax_scale = softmax_scale
84+
self.attention_dropout = attention_dropout
85+
self.attention_type = attention_type
86+
self.layer_number = layer_number
87+
self._npu_flash = None
88+
89+
def _ensure_backend(self):
90+
"""Lazy-initialize NPU FlashAttention backend."""
91+
if self._npu_flash is not None:
92+
return
93+
from transformer_engine_npu.pytorch.attention.dot_product_attention.backends import (
94+
FlashAttention as _NPUFlashAttention,
95+
)
96+
97+
self._npu_flash = _NPUFlashAttention(self.softmax_scale)
98+
99+
@staticmethod
100+
def _layout_to_format(qkv_layout: Optional[str]) -> str:
101+
"""Map TE-FL qkv_layout string to NPU qkv_format."""
102+
if qkv_layout is None:
103+
return "sbhd"
104+
layout = qkv_layout.lower()
105+
if "thd" in layout or layout.startswith("t"):
106+
return "thd"
107+
return "sbhd"
108+
109+
@staticmethod
110+
def _is_bshd_layout(qkv_layout: Optional[str]) -> bool:
111+
"""Whether the separate Q/K/V tensors use batch-major BSHD layout."""
112+
if qkv_layout is None:
113+
return False
114+
layout = qkv_layout.lower()
115+
# Covers bs3hd, bsh3d, and bshd_bshd_bshd.
116+
return layout.startswith("bs")
117+
118+
def _forward_impl(
119+
self,
120+
query_layer: torch.Tensor,
121+
key_layer: torch.Tensor,
122+
value_layer: torch.Tensor,
123+
attention_mask: Optional[Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]] = None,
124+
qkv_layout: Optional[str] = None,
125+
cu_seqlens_q: Optional[torch.Tensor] = None,
126+
cu_seqlens_kv: Optional[torch.Tensor] = None,
127+
max_seqlen_q: Optional[int] = None,
128+
max_seqlen_kv: Optional[int] = None,
129+
attn_mask_type: str = "causal",
130+
window_size: Optional[Tuple[int, int]] = None,
131+
alibi_slopes: Optional[torch.Tensor] = None,
132+
cp_group: Optional[Any] = None,
133+
cp_global_ranks: Optional[List[int]] = None,
134+
cp_stream: Optional[Any] = None,
135+
cp_comm_type: str = "p2p",
136+
fp8: bool = False,
137+
fp8_meta: Optional[Dict[str, Any]] = None,
138+
quantizers: Optional[Any] = None,
139+
inference_params: Optional[Any] = None,
140+
flash_attention_backend: Optional[Any] = None,
141+
fp8_output: bool = False,
142+
num_splits: Optional[int] = 1,
143+
) -> torch.Tensor:
144+
"""Forward pass — adapts TE-FL args to NPU FlashAttention interface.
145+
146+
Only passes: query, key, value, attention_mask, qkv_format,
147+
cu_seqlens_q, cu_seqlens_kv, attn_mask_type
148+
149+
Raises:
150+
NotImplementedError: For features that would produce incorrect results
151+
if silently ignored (window_size, alibi_slopes, cp_group).
152+
153+
Warns:
154+
For features that don't affect correctness but differ from user
155+
expectation (fp8, inference_params).
156+
"""
157+
# --- Validate: features that would silently produce wrong results ---
158+
if window_size is not None and window_size not in ((-1, -1), (-1, 0)):
159+
raise NotImplementedError(
160+
"NPU FlashAttention does not support sliding window attention "
161+
f"(window_size={window_size}). npu_fusion_attention only computes "
162+
"full causal/padding attention. Either disable sliding window or "
163+
"use UnfusedDotProductAttention as fallback."
164+
)
165+
166+
if alibi_slopes is not None:
167+
raise NotImplementedError(
168+
"NPU FlashAttention does not support ALiBi position encoding "
169+
"(alibi_slopes). npu_fusion_attention has no ALiBi parameter. "
170+
"Use RoPE or other position encoding supported by NPU."
171+
)
172+
173+
if cp_group is not None:
174+
raise NotImplementedError(
175+
"NPU FlashAttention does not support Context Parallelism "
176+
"(cp_group). Ring attention / CP requires NPU-specific HCCL "
177+
"implementation which is not yet available."
178+
)
179+
180+
# --- Warn: features that don't break correctness but differ from expectation ---
181+
if fp8:
182+
import warnings
183+
184+
warnings.warn(
185+
"NPU FlashAttention does not support FP8 attention computation. "
186+
"Falling back to BF16/FP16 precision. Results are correct but "
187+
"without FP8 performance optimization.",
188+
stacklevel=2,
189+
)
190+
191+
if inference_params is not None:
192+
import warnings
193+
194+
warnings.warn(
195+
"NPU FlashAttention does not support KV cache (inference_params). "
196+
"Full recomputation will be used. This is correct but slower for "
197+
"autoregressive inference.",
198+
stacklevel=2,
199+
)
200+
201+
# TransformerEngineNPU only accepts sequence-major SBHD or packed THD.
202+
# Convert batch-major BSHD inputs explicitly instead of only relabeling
203+
# their layout, which would swap the semantic batch and sequence axes.
204+
input_is_bshd = self._is_bshd_layout(qkv_layout)
205+
if input_is_bshd:
206+
query_layer = query_layer.transpose(0, 1).contiguous()
207+
key_layer = key_layer.transpose(0, 1).contiguous()
208+
value_layer = value_layer.transpose(0, 1).contiguous()
209+
210+
qkv_format = self._layout_to_format(qkv_layout)
211+
if attn_mask_type in ("causal", "padding_causal", "padding,causal", "causal,padding"):
212+
attention_mask = get_compressed_causal_mask(query_layer.device)
213+
214+
self._ensure_backend()
215+
output = self._npu_flash(
216+
query_layer,
217+
key_layer,
218+
value_layer,
219+
attention_mask=attention_mask,
220+
qkv_format=qkv_format,
221+
cu_seqlens_q=cu_seqlens_q,
222+
cu_seqlens_kv=cu_seqlens_kv,
223+
attn_mask_type=attn_mask_type,
224+
)
225+
226+
if input_is_bshd:
227+
output = output.transpose(0, 1).contiguous()
228+
229+
return output

0 commit comments

Comments
 (0)