Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / components / functional / npu_fusion_attention.py: 91%

190 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"""NPU fusion attention function.""" 

16 

17from __future__ import annotations 

18 

19from collections.abc import Mapping, Sequence 

20from dataclasses import dataclass 

21from numbers import Integral 

22from typing import Any, Optional 

23 

24import torch # pylint: disable=forbidden-backend-import 

25 

26 

27_QUERY_LENGTH_ALIASES = ( 

28 ("actual_seq_len", False), ("actual_q_len", False), ("actual_seq_qlen", False), 

29 ("cu_seq_lens", True), ("cu_seq_lens_q", True), ("cu_seqlens_q", True), 

30) 

31_KEY_LENGTH_ALIASES = ( 

32 ("actual_seq_len", False), ("actual_kv_len", False), ("actual_seq_kvlen", False), 

33 ("cu_seq_lens", True), ("cu_seq_lens_k", True), ("cu_seq_lens_kv", True), 

34 ("cu_seqlens_k", True), ("cu_seqlens_kv", True), 

35) 

36PACKED_SEQUENCE_ARGUMENTS = frozenset( 

37 name for name, _ in _QUERY_LENGTH_ALIASES + _KEY_LENGTH_ALIASES 

38) | {"packed_seq_params"} 

39 

40_COMPRESSED_CAUSAL_MASK_SIZE = 2048 

41 

42 

43@dataclass 

44class _FusionAttentionContext: 

45 """Normalized arguments required by the NPU fusion-attention operator.""" 

46 

47 query: torch.Tensor 

48 key: torch.Tensor 

49 value: torch.Tensor 

50 input_layout: str 

51 attention_mask: Optional[torch.Tensor] 

52 sparse_mode: int 

53 query_lengths: Optional[Sequence[int]] 

54 key_lengths: Optional[Sequence[int]] 

55 batch_size: int 

56 query_length: int 

57 head_dim: int 

58 pre_tokens: int 

59 next_tokens: int 

60 is_packed: bool 

61 valid_rows: Optional[torch.Tensor] 

62 indices: Optional[torch.Tensor] = None 

63 

64 

65def _npu_attention_mask(attention_mask: torch.Tensor) -> torch.Tensor: 

66 """Convert the Transformers attention mask to the NPU mask convention.""" 

67 if attention_mask.dtype == torch.bool: 

68 return torch.logical_not(attention_mask) 

69 return attention_mask != 0 

70 

71 

72def _causal_attention_mask( 

73 query: torch.Tensor, 

74 key: torch.Tensor, 

75 sliding_window: Optional[int], 

76) -> torch.Tensor: 

77 """Build the right-aligned causal mask used by Transformers decoding.""" 

78 query_length = query.shape[2] 

79 key_length = key.shape[2] 

80 query_positions = torch.arange(query_length, device=query.device) 

81 query_positions = query_positions + max(key_length - query_length, 0) 

82 key_positions = torch.arange(key_length, device=query.device) 

83 allowed = key_positions.unsqueeze(0) <= query_positions.unsqueeze(1) 

84 if sliding_window is not None: 

85 allowed &= key_positions.unsqueeze(0) > ( 

86 query_positions.unsqueeze(1) - sliding_window 

87 ) 

88 return torch.logical_not(allowed) 

89 

90 

91def _get_compressed_causal_mask(device: torch.device) -> torch.Tensor: 

92 """Return the mask required by Ascend compressed causal sparse modes.""" 

93 return torch.ones( 

94 (_COMPRESSED_CAUSAL_MASK_SIZE, _COMPRESSED_CAUSAL_MASK_SIZE), 

95 dtype=torch.bool, 

96 device=device, 

97 ).triu(diagonal=1) 

98 

99 

100def _length_list( 

101 value: torch.Tensor | Sequence[int], 

102 *, 

103 includes_zero: bool, 

104) -> list[int]: 

105 """Normalize one cumulative-length representation.""" 

106 if isinstance(value, torch.Tensor): 

107 if value.ndim != 1: 

108 raise ValueError("packed cumulative sequence lengths must be one-dimensional") 

109 value = value.tolist() 

110 if any(isinstance(item, bool) or not isinstance(item, Integral) for item in value): 

111 raise ValueError("packed cumulative sequence lengths must be integers") 

112 lengths = [int(item) for item in value] 

113 if includes_zero: 

114 if not lengths or lengths[0] != 0: 

115 raise ValueError("cu_seq_lens must start with zero") 

116 lengths = lengths[1:] 

117 if not lengths or any(left >= right for left, right in zip([0] + lengths[:-1], lengths)): 

118 raise ValueError("packed cumulative sequence lengths must be strictly increasing") 

119 return lengths 

120 

121 

122def _coalesce_lengths( 

123 kwargs: dict[str, Any], 

124 aliases: tuple[tuple[str, bool], ...], 

125 *, 

126 name: str, 

127) -> list[int] | None: 

128 """Read equivalent length arguments and reject conflicting values.""" 

129 candidates = [] 

130 sources = (("", kwargs), ("packed_seq_params.", kwargs.get("packed_seq_params"))) 

131 for prefix, source in sources: 

132 if source is None: 

133 continue 

134 for alias, includes_zero in aliases: 

135 value = source.get(alias) if isinstance(source, Mapping) else getattr(source, alias, None) 

136 if value is not None: 

137 candidates.append((prefix + alias, _length_list(value, includes_zero=includes_zero))) 

138 if not candidates: 

139 return None 

140 first_alias, first = candidates[0] 

141 for alias, candidate in candidates[1:]: 

142 if candidate != first: 

143 raise ValueError( 

144 f"conflicting {name} values were provided through " 

145 f"{first_alias!r} and {alias!r}" 

146 ) 

147 return first 

148 

149 

150def resolve_packed_sequence_lengths( 

151 kwargs: dict[str, Any], 

152 query_tokens: int, 

153 key_tokens: int, 

154) -> tuple[list[int] | None, list[int] | None]: 

155 """Resolve supported packed-sequence argument aliases. 

156 

157 Args: 

158 kwargs: Attention keyword arguments or a ``packed_seq_params`` carrier. 

159 query_tokens: Number of flattened query tokens. 

160 key_tokens: Number of flattened key/value tokens. 

161 

162 Returns: 

163 Cumulative query and key/value sequence ends without leading zeros. 

164 """ 

165 query_lengths = _coalesce_lengths( 

166 kwargs, _QUERY_LENGTH_ALIASES, name="query sequence lengths", 

167 ) 

168 key_lengths = _coalesce_lengths( 

169 kwargs, _KEY_LENGTH_ALIASES, name="key/value sequence lengths", 

170 ) 

171 if kwargs.get("packed_seq_params") is not None and query_lengths is None and key_lengths is None: 

172 raise ValueError("packed_seq_params must provide cumulative query and key/value sequence lengths") 

173 if (query_lengths is None) != (key_lengths is None): 

174 raise ValueError("packed attention requires both query and key/value lengths") 

175 if query_lengths is not None: 

176 if query_lengths[-1] != query_tokens: 

177 raise ValueError("the final query sequence length must equal the query token count") 

178 if key_lengths[-1] != key_tokens: 

179 raise ValueError( 

180 "the final key/value sequence length must equal the key/value token count" 

181 ) 

182 return query_lengths, key_lengths 

183 

184 

185def _attention_options( 

186 module: torch.nn.Module, 

187 kwargs: dict[str, Any], 

188 query_length: int, 

189 key_length: int, 

190 *, 

191 is_packed: bool, 

192): 

193 """Resolve sparse-window and causal options from kwargs and the module.""" 

194 pre_tokens = kwargs.get("pre_tokens", getattr(module, "pre_tockens", 1048576)) 

195 sparse_mode = kwargs.get("sparse_mode", getattr(module, "sparse_mode", 0)) 

196 sliding_window = kwargs.get("sliding_window") 

197 is_causal = kwargs.get("is_causal", getattr(module, "is_causal", True)) 

198 next_tokens = kwargs.get("next_tokens", getattr(module, "next_tockens", 0 if is_causal else 2147483647)) 

199 if sliding_window is not None: 

200 pre_tokens = sliding_window 

201 elif not is_packed and is_causal and query_length == key_length and pre_tokens < query_length: 

202 sliding_window = pre_tokens + 1 

203 return pre_tokens, next_tokens, sparse_mode, sliding_window, is_causal 

204 

205 

206def _prepare_attention_inputs( 

207 query: torch.Tensor, 

208 key: torch.Tensor, 

209 value: torch.Tensor, 

210 attention_mask: Optional[torch.Tensor], 

211 *, 

212 is_packed: bool, 

213 is_causal: bool, 

214 sliding_window: Optional[int], 

215 sparse_mode: int, 

216) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, str, Optional[torch.Tensor], int]: 

217 """Prepare QKV layout, mask, and sparse mode for the NPU operator.""" 

218 if is_packed: 

219 query = query.transpose(1, 2).reshape(-1, query.shape[1], query.shape[-1]) 

220 key = key.transpose(1, 2).reshape(-1, key.shape[1], key.shape[-1]) 

221 value = value.transpose(1, 2).reshape(-1, value.shape[1], value.shape[-1]) 

222 if attention_mask is None and is_causal: 

223 npu_mask = _get_compressed_causal_mask(query.device) 

224 sparse_mode = 3 

225 else: 

226 npu_mask = None if attention_mask is None else _npu_attention_mask(attention_mask) 

227 return query, key, value, "TND", npu_mask, sparse_mode 

228 if attention_mask is None and is_causal: 

229 if sparse_mode in (2, 4): 

230 return query, key, value, "BNSD", _get_compressed_causal_mask(query.device), sparse_mode 

231 return query, key, value, "BNSD", _causal_attention_mask(query, key, sliding_window), 0 

232 if attention_mask is not None and attention_mask.ndim == 2: 

233 attention_mask = attention_mask.to(device=query.device, dtype=torch.bool)[:, None, None, :] 

234 attention_mask = attention_mask.expand(-1, 1, query.shape[2], -1) 

235 npu_mask = None if attention_mask is None else _npu_attention_mask(attention_mask) 

236 if npu_mask is not None and is_causal and sparse_mode == 0: 

237 npu_mask = npu_mask | _causal_attention_mask(query, key, sliding_window) 

238 return query, key, value, "BNSD", npu_mask, sparse_mode 

239 

240 

241def _prepare_padding_attention( 

242 context: _FusionAttentionContext, 

243 sliding_window: Optional[int], 

244) -> bool: 

245 """Gather valid QKV tokens directly into TND, retaining output indices.""" 

246 attention_mask = context.attention_mask 

247 if attention_mask.shape[-1] != context.query_length: 

248 raise ValueError("the padding mask length must equal the query/key sequence length") 

249 attention_mask = attention_mask.to(device=context.query.device, dtype=torch.bool).expand(context.batch_size, -1) 

250 context.attention_mask = attention_mask 

251 if bool(attention_mask.all()): 

252 context.attention_mask = None 

253 return False 

254 if sliding_window is not None: 

255 # Removing interior holes would change the original window distances. 

256 starts = attention_mask[:, 0].int() + (attention_mask[:, 1:] & ~attention_mask[:, :-1]).sum(dim=-1) 

257 if bool((starts > 1).any()): 

258 return False 

259 context.indices = attention_mask.flatten().nonzero(as_tuple=False).flatten() 

260 lengths = attention_mask.sum(dim=-1, dtype=torch.int32) 

261 # Empty samples have no selected tokens and need no segment in the NPU call. 

262 context.query_lengths = tuple(lengths[lengths > 0].cumsum(0).cpu().tolist()) 

263 context.key_lengths = context.query_lengths 

264 context.query, context.key, context.value = ( 

265 tensor.transpose(1, 2).flatten(0, 1).index_select(0, context.indices) 

266 for tensor in (context.query, context.key, context.value) 

267 ) 

268 context.input_layout = "TND" 

269 context.is_packed = True 

270 context.attention_mask = _get_compressed_causal_mask(context.query.device) 

271 context.sparse_mode = 3 if sliding_window is None else 4 

272 if sliding_window is not None: 

273 context.pre_tokens = sliding_window - 1 

274 return True 

275 

276 

277def _prepare_attention_mask( 

278 context: _FusionAttentionContext, 

279 is_causal: bool, 

280 sliding_window: Optional[int], 

281) -> None: 

282 """Select padding, compressed causal/window, or explicit-mask preparation.""" 

283 equal_lengths = context.query.shape[2] == context.key.shape[2] 

284 compressible = not context.is_packed and equal_lengths and is_causal and context.next_tokens == 0 

285 compressible = compressible and (sliding_window is None or sliding_window > 0) 

286 if compressible and context.attention_mask is not None and context.attention_mask.ndim == 2: 

287 if _prepare_padding_attention(context, sliding_window): 

288 return 

289 if not context.is_packed and context.attention_mask is None and is_causal: 

290 context.sparse_mode = 0 

291 if compressible: 

292 if sliding_window is not None: 

293 context.sparse_mode = 4 

294 context.pre_tokens = sliding_window - 1 

295 elif context.query_length > _COMPRESSED_CAUSAL_MASK_SIZE: 

296 context.sparse_mode = 2 

297 prepared = _prepare_attention_inputs( 

298 context.query, context.key, context.value, context.attention_mask, 

299 is_packed=context.is_packed, 

300 is_causal=is_causal, 

301 sliding_window=sliding_window, 

302 sparse_mode=context.sparse_mode, 

303 ) 

304 ( 

305 context.query, context.key, context.value, 

306 context.input_layout, context.attention_mask, context.sparse_mode, 

307 ) = prepared 

308 

309 

310def _prepare_fusion_attention_context( 

311 module: torch.nn.Module, 

312 query: torch.Tensor, 

313 key: torch.Tensor, 

314 value: torch.Tensor, 

315 attention_mask: Optional[torch.Tensor], 

316 kwargs: dict[str, Any], 

317) -> _FusionAttentionContext: 

318 """Normalize model inputs into one NPU fusion-attention invocation.""" 

319 query_lengths, key_lengths = resolve_packed_sequence_lengths( 

320 kwargs, 

321 query.shape[0] * query.shape[2], 

322 key.shape[0] * key.shape[2], 

323 ) 

324 options = _attention_options( 

325 module, kwargs, query.shape[2], key.shape[2], is_packed=query_lengths is not None, 

326 ) 

327 context = _FusionAttentionContext( 

328 query, key, value, "BNSD", attention_mask, options[2], 

329 query_lengths, 

330 key_lengths, 

331 query.shape[0], 

332 query.shape[2], 

333 query.shape[-1], 

334 options[0], 

335 options[1], 

336 query_lengths is not None, 

337 None, 

338 ) 

339 _prepare_attention_mask(context, options[4], options[3]) 

340 if not context.is_packed and context.attention_mask is not None and context.sparse_mode == 0: 

341 context.valid_rows = (~context.attention_mask).any(dim=-1) 

342 # Give fully masked rows one finite softmax entry, then suppress their 

343 # output and upstream gradient. Some NPU kernels otherwise return nonzero rows. 

344 context.attention_mask[..., 0] &= context.valid_rows 

345 return context 

346 

347 

348def npu_fusion_attention_forward( 

349 module: torch.nn.Module, 

350 query: torch.Tensor, 

351 key: torch.Tensor, 

352 value: torch.Tensor, 

353 attention_mask: Optional[torch.Tensor], 

354 dropout: float = 0.0, 

355 scaling: Optional[float] = None, 

356 **kwargs: Any, 

357) -> tuple[torch.Tensor, None]: 

358 """Compute NPU fusion attention using the Transformers AttentionInterface contract. 

359 

360 Args: 

361 module: Owning Transformers attention module. 

362 query: Query tensor in BNSD layout. 

363 key: Key tensor in BNSD layout. 

364 value: Value tensor in BNSD layout. 

365 attention_mask: Attention mask using the Transformers mask convention. 

366 dropout: Dropout probability. 

367 scaling: Attention score scaling factor. 

368 **kwargs: Additional attention arguments. 

369 

370 Returns: 

371 Attention output in BSND layout and ``None`` for attention weights. 

372 

373 Raises: 

374 ValueError: If sparse attention indices or inconsistent sequence lengths 

375 are supplied. 

376 

377 Note: 

378 QKV inputs use four-dimensional BNSD layout. Packed inputs are flattened 

379 to TND when cumulative sequence lengths are supplied through the project 

380 ``actual_*`` aliases or Transformers ``cu_seq_lens_*`` names. Float16, 

381 bfloat16, and float32 inputs have been verified. A boolean mask uses 

382 ``True`` for positions that participate in attention; an additive mask 

383 uses zero for those positions. A two-dimensional padding mask uses 

384 one/True for valid tokens. Equal-length causal padding attention uses 

385 variable-length attention and restores padded queries as zeros. Windowed 

386 attention uses this path for contiguous padding; masks with interior 

387 holes retain their original positional window through an explicit mask. 

388 """ 

389 # Length parsing is also used by CPU CP validation; load the optional kernel only here. 

390 import torch_npu # pylint: disable=C0415 

391 

392 if kwargs.get("indices") is not None: 

393 raise ValueError( 

394 "npu_fusion_attention_forward does not consume sparse attention indices; " 

395 "select a DSA sparse-attention implementation instead." 

396 ) 

397 context = _prepare_fusion_attention_context( 

398 module, 

399 query, 

400 key, 

401 value, 

402 attention_mask, 

403 kwargs, 

404 ) 

405 if context.indices is not None and context.indices.numel() == 0: 

406 output = query.new_zeros((query.shape[0], query.shape[2], query.shape[1], query.shape[3])) 

407 return output + context.query.sum() + context.key.sum() + context.value.sum(), None 

408 output = torch_npu.npu_fusion_attention( 

409 context.query.contiguous(), 

410 context.key.contiguous(), 

411 context.value.contiguous(), 

412 context.query.shape[1], 

413 context.input_layout, 

414 pse=None, 

415 padding_mask=None, 

416 atten_mask=context.attention_mask, 

417 scale=context.head_dim**-0.5 if scaling is None else scaling, 

418 pre_tockens=context.pre_tokens, 

419 next_tockens=context.next_tokens, 

420 keep_prob=1.0 - dropout, 

421 inner_precise=0, 

422 sparse_mode=context.sparse_mode, 

423 actual_seq_qlen=context.query_lengths, 

424 actual_seq_kvlen=context.key_lengths, 

425 )[0] 

426 if context.valid_rows is not None: 

427 output = output.masked_fill(~context.valid_rows.unsqueeze(-1), 0.0) 

428 if context.indices is not None: 

429 restored = output.new_zeros((context.batch_size * context.query_length, *output.shape[1:])) 

430 output = restored.index_copy(0, context.indices, output) 

431 if context.is_packed: 

432 output = output.reshape( 

433 context.batch_size, 

434 context.query_length, 

435 output.shape[1], 

436 output.shape[2], 

437 ) 

438 return output, None 

439 return output.transpose(1, 2), None