Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / components / modules / gated_delta_net.py: 64%

117 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-15 07:18 +0800

1# Copyright 2026 Huawei Technologies Co., Ltd 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14# ============================================================================ 

15"""Gated DeltaNet eager oracle and explicit local-backend dispatcher.""" 

16# pylint: disable=forbidden-backend-import,missing-public-type-hints 

17# pylint: disable=missing-public-docstring,not-callable 

18 

19__all__ = ["chunk_gated_delta_rule", "is_triton_gdn_available", "torch_chunk_gated_delta_rule"] 

20 

21import importlib 

22import importlib.metadata 

23import importlib.util 

24import re 

25from typing import Optional 

26 

27import torch 

28from torch.nn import functional as F 

29 

30from hyper_parallel.components.functional.linear_attention_packed import ( 

31 packed_reference, 

32) 

33 

34_GDN_BACKENDS = frozenset({"eager", "triton", "ascendc"}) 

35_MIN_TRITON_ASCEND_VERSION = (3, 2, 1) 

36_TRITON_GDN_HEAD_DIM = 128 

37_TRITON_GDN_CHUNK_SIZE = 64 

38 

39 

40def _l2norm(x: torch.Tensor, dim: int = -1, eps: float = 1e-6) -> torch.Tensor: 

41 """L2-normalize along ``dim``.""" 

42 # ``(x * x).sum`` (MulBackward) instead of ``x.pow(2).sum`` (PowBackward): 

43 # equal math, NPU yields ULP-different gradients across the two ops. 

44 return x * torch.rsqrt((x * x).sum(dim=dim, keepdim=True) + eps) 

45 

46 

47def torch_chunk_gated_delta_rule( 

48 query: torch.Tensor, 

49 key: torch.Tensor, 

50 value: torch.Tensor, 

51 g: torch.Tensor, 

52 beta: torch.Tensor, 

53 chunk_size: int = 64, 

54 initial_state: Optional[torch.Tensor] = None, 

55 output_final_state: bool = False, 

56 use_qk_l2norm_in_kernel: bool = False, 

57 cu_seqlens: Optional[torch.Tensor] = None, 

58): 

59 """Chunked gated delta-rule linear attention (pure torch reference). 

60 

61 fp32 internal compute, output cast back to input dtype. 

62 

63 Shapes 

64 ------ 

65 query / key: ``(B, S, num_k_heads, head_k_dim)`` 

66 value: ``(B, S, num_v_heads, head_v_dim)`` (here num_k_heads == num_v_heads) 

67 g: ``(B, S, num_v_heads)`` 

68 beta: ``(B, S, num_v_heads)`` 

69 """ 

70 if cu_seqlens is not None: 

71 return packed_reference( 

72 torch_chunk_gated_delta_rule, (query, key, value, g, beta), cu_seqlens, 

73 initial_state=initial_state, output_final_state=output_final_state, 

74 use_qk_l2norm_in_kernel=use_qk_l2norm_in_kernel, chunk_size=chunk_size, 

75 ) 

76 initial_dtype = query.dtype 

77 if use_qk_l2norm_in_kernel: 

78 query = _l2norm(query, dim=-1, eps=1e-6) 

79 key = _l2norm(key, dim=-1, eps=1e-6) 

80 # (B, S, H, D) → (B, H, S, D), fp32 

81 query, key, value, beta, g = [ 

82 x.transpose(1, 2).contiguous().to(torch.float32) 

83 for x in (query, key, value, beta, g) 

84 ] 

85 

86 batch_size, num_heads, sequence_length, k_head_dim = key.shape 

87 v_head_dim = value.shape[-1] 

88 pad_size = (chunk_size - sequence_length % chunk_size) % chunk_size 

89 query = F.pad(query, (0, 0, 0, pad_size)) 

90 key = F.pad(key, (0, 0, 0, pad_size)) 

91 value = F.pad(value, (0, 0, 0, pad_size)) 

92 beta = F.pad(beta, (0, pad_size)) 

93 g = F.pad(g, (0, pad_size)) 

94 total_sequence_length = sequence_length + pad_size 

95 scale = 1 / (query.shape[-1] ** 0.5) 

96 query = query * scale 

97 

98 v_beta = value * beta.unsqueeze(-1) 

99 k_beta = key * beta.unsqueeze(-1) 

100 # (B, H, S, D) → (B, H, n_chunks, chunk_size, D) 

101 query, key, value, k_beta, v_beta = [ 

102 x.reshape(x.shape[0], x.shape[1], -1, chunk_size, x.shape[-1]) 

103 for x in (query, key, value, k_beta, v_beta) 

104 ] 

105 g = g.reshape(g.shape[0], g.shape[1], -1, chunk_size) 

106 mask = torch.triu( 

107 torch.ones(chunk_size, chunk_size, dtype=torch.bool, device=query.device), 

108 diagonal=0, 

109 ) 

110 

111 # chunk decay 

112 g = g.cumsum(dim=-1) 

113 decay_mask = ((g.unsqueeze(-1) - g.unsqueeze(-2)).tril().exp().float()).tril() 

114 attn = -((k_beta @ key.transpose(-1, -2)) * decay_mask).masked_fill(mask, 0) 

115 for i in range(1, chunk_size): 

116 row = attn[..., i, :i].clone() 

117 sub = attn[..., :i, :i].clone() 

118 attn[..., i, :i] = row + (row.unsqueeze(-1) * sub).sum(-2) 

119 attn = attn + torch.eye(chunk_size, dtype=attn.dtype, device=attn.device) 

120 value = attn @ v_beta 

121 k_cumdecay = attn @ (k_beta * g.exp().unsqueeze(-1)) 

122 last_recurrent_state = ( 

123 torch.zeros(batch_size, num_heads, k_head_dim, v_head_dim).to(value) 

124 if initial_state is None 

125 else initial_state.to(value) 

126 ) 

127 core_attn_out = torch.zeros_like(value) 

128 

129 # for each chunk 

130 for i in range(0, total_sequence_length // chunk_size): 

131 q_i, k_i, v_i = query[:, :, i], key[:, :, i], value[:, :, i] 

132 attn = q_i @ k_i.transpose(-1, -2) * decay_mask[:, :, i] 

133 v_prime = k_cumdecay[:, :, i] @ last_recurrent_state 

134 v_new = v_i - v_prime 

135 attn_inter = (q_i * g[:, :, i, :, None].exp()) @ last_recurrent_state 

136 core_attn_out[:, :, i] = attn_inter + attn @ v_new 

137 last_recurrent_state = ( 

138 last_recurrent_state * g[:, :, i, -1, None, None].exp() 

139 + (k_i * (g[:, :, i, -1, None] - g[:, :, i]).exp()[..., None]).transpose(-1, -2) @ v_new 

140 ) 

141 

142 if not output_final_state: 

143 last_recurrent_state = None 

144 core_attn_out = core_attn_out.reshape( 

145 core_attn_out.shape[0], core_attn_out.shape[1], -1, core_attn_out.shape[-1], 

146 ) 

147 core_attn_out = core_attn_out[:, :, :sequence_length] 

148 core_attn_out = core_attn_out.transpose(1, 2).contiguous().to(initial_dtype) 

149 return core_attn_out, last_recurrent_state 

150 

151 

152def _parse_version(version_text: str) -> tuple[int, int, int]: 

153 """Return a three-component numeric version tuple.""" 

154 parts = [] 

155 for part in version_text.split("+")[0].split(".")[:3]: 

156 match = re.match(r"\d+", part) 

157 if match is not None: 

158 parts.append(int(match.group())) 

159 return tuple((parts + [0, 0, 0])[:3]) 

160 

161 

162def _is_triton_gdn_input_supported( 

163 query: torch.Tensor, 

164 key: Optional[torch.Tensor], 

165 value: Optional[torch.Tensor], 

166 g: Optional[torch.Tensor], 

167 beta: Optional[torch.Tensor], 

168) -> bool: 

169 """Check the fixed Qwen3.5 GDN contract validated by this backend.""" 

170 if any(tensor is None for tensor in (key, value, g, beta)): 

171 return False 

172 if not ( 

173 query.device.type == "npu" 

174 and query.dtype == key.dtype == value.dtype == beta.dtype == torch.bfloat16 

175 and g.dtype == torch.float32 

176 and query.ndim == key.ndim == value.ndim == 4 

177 and g.ndim == beta.ndim == 3 

178 and query.shape == key.shape 

179 and query.shape[:3] == value.shape[:3] == g.shape == beta.shape 

180 and query.shape[-1] == value.shape[-1] == _TRITON_GDN_HEAD_DIM 

181 ): 

182 return False 

183 return all( 

184 tensor.device == query.device 

185 for tensor in (key, value, g, beta) 

186 ) 

187 

188 

189def is_triton_gdn_available( 

190 query: Optional[torch.Tensor] = None, 

191 key: Optional[torch.Tensor] = None, 

192 value: Optional[torch.Tensor] = None, 

193 g: Optional[torch.Tensor] = None, 

194 beta: Optional[torch.Tensor] = None, 

195 chunk_size: int = _TRITON_GDN_CHUNK_SIZE, 

196) -> bool: 

197 """Return whether the validated Triton-Ascend GDN backend is available.""" 

198 if chunk_size != _TRITON_GDN_CHUNK_SIZE: 

199 return False 

200 if query is not None and not _is_triton_gdn_input_supported( 

201 query, key, value, g, beta, 

202 ): 

203 return False 

204 try: 

205 version_text = importlib.metadata.version("triton-ascend") 

206 except importlib.metadata.PackageNotFoundError: 

207 return False 

208 version = _parse_version(version_text) 

209 if version < _MIN_TRITON_ASCEND_VERSION: 

210 return False 

211 try: 

212 importlib.import_module("triton") 

213 return importlib.util.find_spec("triton.backends.ascend") is not None 

214 except (AttributeError, ImportError, ModuleNotFoundError): 

215 return False 

216 

217 

218def chunk_gated_delta_rule( 

219 query: torch.Tensor, 

220 key: torch.Tensor, 

221 value: torch.Tensor, 

222 g: torch.Tensor, 

223 beta: torch.Tensor, 

224 chunk_size: int = 64, 

225 initial_state: Optional[torch.Tensor] = None, 

226 output_final_state: bool = False, 

227 use_qk_l2norm_in_kernel: bool = False, 

228 backend: str = "eager", 

229 cu_seqlens: Optional[torch.Tensor] = None, 

230): 

231 """Dispatch GDN to an explicitly selected eager, Triton or Ascend C backend.""" 

232 if query.ndim == 3: 

233 output, final = chunk_gated_delta_rule( 

234 *(tensor.unsqueeze(0) for tensor in (query, key, value, g, beta)), 

235 initial_state=initial_state, output_final_state=output_final_state, 

236 use_qk_l2norm_in_kernel=use_qk_l2norm_in_kernel, chunk_size=chunk_size, 

237 backend=backend, cu_seqlens=cu_seqlens) 

238 return output.squeeze(0), final 

239 backend = backend.lower() 

240 if backend not in _GDN_BACKENDS: 

241 raise ValueError( 

242 f"unsupported GDN backend {backend!r}; " 

243 f"expected one of {sorted(_GDN_BACKENDS)}." 

244 ) 

245 if backend == "eager": 

246 return torch_chunk_gated_delta_rule( 

247 query, 

248 key, 

249 value, 

250 g=g, 

251 beta=beta, 

252 chunk_size=chunk_size, 

253 initial_state=initial_state, 

254 output_final_state=output_final_state, 

255 use_qk_l2norm_in_kernel=use_qk_l2norm_in_kernel, 

256 cu_seqlens=cu_seqlens, 

257 ) 

258 if backend == "ascendc": 

259 if initial_state is not None or output_final_state: 

260 raise NotImplementedError("GDN Ascend C currently supports zero-state local or pure Ulysses training only.") 

261 if chunk_size != 64: 

262 raise ValueError("GDN Ascend C requires chunk_size=64.") 

263 # Preserve the existing GDN normalization graph outside the native core. 

264 if use_qk_l2norm_in_kernel: 

265 query, key = _l2norm(query), _l2norm(key) 

266 # Keep optional native dependencies out of eager and Triton imports. 

267 from hyper_parallel.components.functional.gated_delta_net_ascendc import ( # pylint: disable=import-outside-toplevel 

268 chunk_gated_delta_rule_ascendc, 

269 ) 

270 return chunk_gated_delta_rule_ascendc(query, key, value, g, beta, cu_seqlens=cu_seqlens) 

271 if not is_triton_gdn_available( 

272 query, key, value, g, beta, chunk_size=chunk_size, 

273 ): 

274 raise RuntimeError( 

275 "GDN backend='triton' requires triton-ascend >= 3.2.1 " 

276 "installation with the Ascend backend and NPU inputs " 

277 "q/k/v/beta=bf16, g=fp32, head_k_dim=head_v_dim=128, " 

278 "and chunk_size=64." 

279 ) 

280 

281 from hyper_parallel.components.functional.gated_delta_net import ( # pylint: disable=import-outside-toplevel 

282 chunk_gated_delta_rule as triton_chunk_gated_delta_rule, 

283 ) 

284 

285 return triton_chunk_gated_delta_rule( 

286 query, 

287 key, 

288 value, 

289 g, 

290 beta, 

291 initial_state=initial_state, 

292 output_final_state=output_final_state, 

293 use_qk_l2norm_in_kernel=use_qk_l2norm_in_kernel, 

294 chunk_size=chunk_size, 

295 cu_seqlens=cu_seqlens, 

296 )