Skip to content

Commit 0233ddf

Browse files
[Performance][310P] Replace l2norm with RMSNorm operator on 310P (vllm-project#10408)
### What this PR does / why we need it? This pull request optimizes L2 normalization for the 310P NPU by introducing a custom l2norm_310p function that leverages the npu_rms_norm kernel, replacing standard PyTorch normalization in several FLA operators. However, the current implementation has a mathematical discrepancy where the epsilon parameter is not scaled by the dimension size, leading to potential numerical inaccuracies. Additionally, it lacks a CPU fallback, which will cause crashes when run on non-NPU devices. ### Does this PR introduce _any_ user-facing change? None ### How was this patch tested? - vLLM version: v0.23.0 - vLLM main: vllm-project/vllm@967c5c3 Signed-off-by: YangShuai52 <shuaiyang047@163.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4fbf24f commit 0233ddf

6 files changed

Lines changed: 77 additions & 12 deletions

File tree

tests/ut/_310p/ops/test_chunk_gated_delta_rule_310.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
1+
from unittest.mock import patch
2+
3+
import pytest
14
import torch
5+
import torch_npu
26

37
from vllm_ascend._310p.ops.fla.chunk_gated_delta_rule import chunk_gated_delta_rule_pytorch
48

59

10+
def _cpu_rms_norm(x, weight, eps):
11+
"""CPU fallback for torch_npu.npu_rms_norm used by l2norm_310p on CPU runners."""
12+
orig_dtype = x.dtype
13+
x32 = x.float()
14+
var = x32.pow(2).mean(-1, keepdim=True)
15+
x32 = x32 * torch.rsqrt(var + eps)
16+
out = (x32 * weight.float()).to(orig_dtype)
17+
return out, None
18+
19+
20+
@pytest.fixture(autouse=True)
21+
def _mock_npu_rms_norm():
22+
# conftest stubs npu_rms_norm with a bare MagicMock(); override with a CPU impl.
23+
with patch.object(torch_npu, "npu_rms_norm", side_effect=_cpu_rms_norm, create=True):
24+
yield
25+
26+
627
def test_chunk_gated_delta_rule_310_output_shape_and_dtype():
728
torch.manual_seed(0)
829

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from .chunk_gated_delta_rule import chunk_gated_delta_rule_310
22
from .fused_gdn_gating import fused_gdn_gating_pytorch
33
from .fused_recurrent_gated_delta_rule import fused_recurrent_gated_delta_rule_pytorch
4+
from .l2norm import l2norm_310p
45

56
__all__ = [
67
"fused_gdn_gating_pytorch",
78
"fused_recurrent_gated_delta_rule_pytorch",
89
"chunk_gated_delta_rule_310",
10+
"l2norm_310p",
911
]

vllm_ascend/_310p/ops/fla/chunk_gated_delta_rule.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import torch
2121
import torch.nn.functional as F
2222

23+
from vllm_ascend._310p.ops.fla.l2norm import l2norm_310p
24+
2325
CHUNK_SIZE = 64
2426

2527

@@ -105,8 +107,8 @@ def _torch_chunk_gated_delta_rule_chunked(
105107
"""
106108
initial_dtype = query.dtype
107109
if use_qk_l2norm_in_kernel:
108-
query = F.normalize(query, p=2, dim=-1, eps=1e-6).to(query.dtype)
109-
key = F.normalize(key, p=2, dim=-1, eps=1e-6).to(key.dtype)
110+
query = l2norm_310p(query)
111+
key = l2norm_310p(key)
110112

111113
query, key, value, beta, g = [
112114
x.transpose(1, 2).contiguous().to(torch.float32) for x in (query, key, value, beta, g)
@@ -200,7 +202,7 @@ def _require_ascend_chunk_ops(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor)
200202
def _maybe_l2norm(x: torch.Tensor, enabled: bool) -> torch.Tensor:
201203
if not enabled:
202204
return x
203-
return F.normalize(x.to(torch.float32), p=2, dim=-1, eps=1e-6).to(x.dtype)
205+
return l2norm_310p(x)
204206

205207

206208
def _pad_bthd_to_chunk(

vllm_ascend/_310p/ops/fla/fused_recurrent_gated_delta_rule.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import torch
2-
import torch.nn.functional as F
2+
3+
from vllm_ascend._310p.ops.fla.l2norm import l2norm_310p
34

45

56
def _maybe_l2norm(x: torch.Tensor, enabled: bool) -> torch.Tensor:
67
if not enabled:
78
return x
8-
return F.normalize(x, p=2, dim=-1, eps=1e-6).to(x.dtype)
9+
return l2norm_310p(x)
910

1011

1112
def _expand_to_hv(x: torch.Tensor, hv: int) -> torch.Tensor:

vllm_ascend/_310p/ops/fla/gdn_310.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919

2020
import torch
21-
import torch.nn.functional as F
2221
from vllm.forward_context import get_forward_context
2322
from vllm.model_executor.layers.mamba.gdn.base import GatedDeltaNetAttention
2423
from vllm.v1.attention.backend import AttentionMetadata # type: ignore
@@ -27,6 +26,7 @@
2726

2827
from vllm_ascend._310p.ops.fla.chunk_gated_delta_rule import chunk_gated_delta_rule_310
2928
from vllm_ascend._310p.ops.fla.fused_gdn_gating import fused_gdn_gating_pytorch
29+
from vllm_ascend._310p.ops.fla.l2norm import l2norm_310p
3030
from vllm_ascend.ascend_forward_context import _EXTRA_CTX
3131
from vllm_ascend.attention.utils import maybe_save_kv_layer_to_connector
3232
from vllm_ascend.compilation.acl_graph import get_draft_graph_params, get_graph_params
@@ -111,10 +111,6 @@ def _register_310_conv1d_buffer_replay(
111111
graph_params.conv1d_events[num_actual_tokens].append(None)
112112

113113

114-
def _l2norm(x: torch.Tensor, eps: float = 1e-6) -> torch.Tensor:
115-
return F.normalize(x.to(torch.float32), p=2, dim=-1, eps=eps).to(x.dtype)
116-
117-
118114
def _flatten_state_indices(
119115
ssm_state_indices: torch.Tensor,
120116
cu_seqlens: torch.Tensor,
@@ -161,8 +157,8 @@ def npu_recurrent_gated_delta_rule_310(
161157
use_qk_l2norm_in_kernel: bool = True,
162158
) -> torch.Tensor:
163159
if use_qk_l2norm_in_kernel:
164-
q = _l2norm(q)
165-
k = _l2norm(k)
160+
q = l2norm_310p(q)
161+
k = l2norm_310p(k)
166162

167163
total_tokens = v.shape[1]
168164
flat_state_indices = _flatten_state_indices(ssm_state_indices, cu_seqlens, total_tokens)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#
2+
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
3+
# This file is a part of the vllm-ascend project.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
# mypy: ignore-errors
17+
18+
from __future__ import annotations
19+
20+
import math
21+
22+
import torch
23+
import torch_npu
24+
from vllm.model_executor.layers.fla.ops.utils import tensor_cache
25+
26+
27+
@tensor_cache
28+
def _l2norm_unit_weight(dim: int, dtype: torch.dtype, device: torch.device) -> torch.Tensor:
29+
# RMSNorm with weight 1/sqrt(dim) matches L2 norm: x / sqrt(sum(x^2)).
30+
return torch.full((dim,), 1.0 / math.sqrt(dim), dtype=dtype, device=device)
31+
32+
33+
def l2norm_310p(x: torch.Tensor, eps: float = 1e-6) -> torch.Tensor:
34+
"""L2-normalize the last dimension using the 310P NPU RMSNorm kernel."""
35+
orig_shape = x.shape
36+
dim = x.shape[-1]
37+
x_2d = x.reshape(-1, dim).contiguous()
38+
weight = _l2norm_unit_weight(dim, x.dtype, x.device)
39+
# RMSNorm: y = x / sqrt(mean(x^2) + eps_rms) * weight
40+
# With weight=1/sqrt(dim), this equals x / sqrt(sum(x^2) + dim * eps_rms).
41+
# L2 norm needs y = x / sqrt(sum(x^2) + eps), so eps_rms = eps / dim.
42+
y, _ = torch_npu.npu_rms_norm(x_2d, weight, eps / dim)
43+
return y.reshape(orig_shape)

0 commit comments

Comments
 (0)