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

171 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"""SparseFlashMla forward/backward adapter for CSA with scalar attention sinks.""" 

16 

17from __future__ import annotations 

18 

19from dataclasses import dataclass 

20from functools import lru_cache 

21from itertools import accumulate 

22from typing import Any, Callable 

23 

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

25 

26 

27def pack_ranges(tensor: torch.Tensor, ranges: tuple[tuple[int, int], ...]) -> torch.Tensor: 

28 """Pack token ranges without copying an already contiguous partition. 

29 

30 Args: 

31 tensor: Source tensor with tokens on the first dimension. 

32 ranges: Nonempty, ordered half-open token intervals. 

33 

34 Returns: 

35 Contiguous packed tokens; autograd restores gradients to the source. 

36 """ 

37 if all(left[1] == right[0] for left, right in zip(ranges, ranges[1:])): 

38 start, end = ranges[0][0], ranges[-1][1] 

39 if start == 0 and end == tensor.shape[0]: 

40 return tensor.contiguous() 

41 return tensor[start:end].contiguous() 

42 return torch.cat([tensor[start:end] for start, end in ranges], dim=0).contiguous() 

43 

44 

45@dataclass(frozen=True) 

46class CSATNDPlan: 

47 """Document-local lengths and canonical K offsets; no training activations.""" 

48 

49 segments: tuple[tuple[int, int, int, int, int], ...] 

50 q_ranges: tuple[tuple[int, int], ...] 

51 k_ranges: tuple[tuple[int, int], ...] 

52 q_lengths: tuple[int, ...] 

53 k_lengths: tuple[int, ...] 

54 residuals: tuple[int, ...] 

55 

56 def query(self, tensor: torch.Tensor) -> torch.Tensor: 

57 """Pack a [1,Q,...] tensor in the plan's document order.""" 

58 return pack_ranges(tensor.squeeze(0), self.q_ranges) 

59 

60 def key(self, tensor: torch.Tensor, key_offset: int = 0) -> torch.Tensor: 

61 """Pack global document prefixes using the physical origin of a [1,K,...] bank.""" 

62 ranges = self.k_ranges if not key_offset else tuple((start-key_offset, end-key_offset) 

63 for start, end in self.k_ranges) 

64 return pack_ranges(tensor.squeeze(0), ranges) 

65 

66 def lengths(self, device: torch.device) -> tuple: 

67 """Reuse device length tensors, including across inference and recomputation.""" 

68 return _device_lengths(self, device) 

69 

70 def indexer_options(self, device: torch.device, ratio: int) -> dict: 

71 """Share the document partition and causal mask between Indexer and KL.""" 

72 cu_q, cu_k, residuals, _ = self.lengths(device) 

73 return {"cu_seqlens_q": cu_q, "cu_seqlens_k": cu_k, 

74 "cmp_residual_k": residuals if ratio > 1 else None, 

75 "layout_q": "TND", "layout_k": "TND", "mask_mode": 3, "cmp_ratio": ratio} 

76 

77 def local_indices(self, indices: torch.Tensor, padded_width: int = 0) -> torch.Tensor: 

78 """Translate bank IDs; pad only when the consumer explicitly requires a width.""" 

79 selected = self.query(indices) 

80 offsets = self.lengths(indices.device)[3] 

81 selected = torch.where(selected >= 0, selected - offsets, -1).int() 

82 if selected.shape[-1] < padded_width: 

83 selected = torch.nn.functional.pad(selected, (0, padded_width-selected.shape[-1]), value=-1) 

84 return selected.unsqueeze(1).contiguous() 

85 

86 def covers_queries(self, length: int) -> bool: 

87 """Return whether packed rows already follow the complete original Q order.""" 

88 return (self.q_ranges[0][0] == 0 and self.q_ranges[-1][1] == length 

89 and all(left[1] == right[0] for left, right in zip(self.q_ranges, self.q_ranges[1:]))) 

90 

91 def restore(self, part: torch.Tensor, output: torch.Tensor) -> None: 

92 """Write packed query rows into their original positions, preserving autograd.""" 

93 cursor = 0 

94 for start, end in self.q_ranges: 

95 output[0, start:end] = part[cursor:cursor+end-start] 

96 cursor += end-start 

97 

98 

99@lru_cache(maxsize=128) 

100def csa_tnd_plan(segments: tuple, ratio: int, has_keys: bool = True) -> CSATNDPlan | None: 

101 """Separate true SWA-only/empty-prefix queries without inventing valid keys.""" 

102 active = tuple(item for item in segments 

103 if item[1] > item[0] and (bool(ratio and item[3] > item[2]) == has_keys)) 

104 if not active: 

105 return None 

106 return CSATNDPlan( 

107 active, tuple((s, e) for s, e, _, _, _ in active), tuple((s, e) for _, _, s, e, _ in active), 

108 tuple(e-s for s, e, _, _, _ in active), tuple(e-s for _, _, s, e, _ in active), 

109 tuple(r for _, _, _, _, r in active), 

110 ) 

111 

112 

113@lru_cache(maxsize=128) 

114@torch.inference_mode(False) 

115def _device_lengths(plan: CSATNDPlan, device: torch.device) -> tuple: 

116 """Cache by the complete document partition, not merely by total Q/K sizes.""" 

117 cu_q = torch.tensor((0, *accumulate(plan.q_lengths)), device=device, dtype=torch.int32) 

118 cu_k = torch.tensor((0, *accumulate(plan.k_lengths)), device=device, dtype=torch.int32) 

119 residuals = torch.tensor(plan.residuals, device=device, dtype=torch.int32) 

120 offsets = torch.tensor([s for s, _ in plan.k_ranges], device=device, dtype=torch.int32) 

121 lengths = torch.tensor(plan.q_lengths, device=device, dtype=torch.int64) 

122 offsets = torch.repeat_interleave(offsets, lengths, output_size=sum(plan.q_lengths)).unsqueeze(1) 

123 return cu_q, cu_k, residuals, offsets 

124 

125 

126@lru_cache(maxsize=1) 

127def _load_attention_ops() -> tuple[Callable[..., Any], Callable[..., Any], Callable[..., Any]]: 

128 """Resolve attention forward, backward and metadata through the official binding.""" 

129 # Lazy import leaves the eager baseline usable without the NPU extension. 

130 try: 

131 from cann_ops_transformer import ( # pylint: disable=import-outside-toplevel 

132 sparse_flash_mla as sparse_flash_mla_op, 

133 sparse_flash_mla_grad, sparse_flash_mla_metadata, 

134 ) 

135 except ImportError as err: 

136 raise RuntimeError( 

137 "use_fused_ops=True requires cann_ops_transformer.sparse_flash_mla, sparse_flash_mla_grad " 

138 "and sparse_flash_mla_metadata. " 

139 "See hyper_parallel/components/functional/README.md for ops-transformer installation, " 

140 "environment setup and tested versions." 

141 ) from err 

142 return sparse_flash_mla_op, sparse_flash_mla_grad, sparse_flash_mla_metadata 

143 

144 

145def supports_fused_attention_inputs(query: torch.Tensor, ratio: int, window: int, topk: int = 512) -> bool: 

146 """Check the validated attention input contract without importing operators. 

147 

148 Args: 

149 query: Main Q [batch, heads, queries, dimension]. 

150 ratio: Model ratio, zero for a pure SWA layer. 

151 window: Raw attention window, including the current token. 

152 topk: Logical compressed selection width, before padding; zero means no complete compressed key. 

153 """ 

154 if query.device.type != "npu" or query.dtype != torch.bfloat16: 

155 return False 

156 if query.shape[-1] != 512 or window != 128 or ratio not in (0, 1, 2): 

157 return False 

158 if query.shape[1] not in (1, 2, 4, 8, 16, 32, 64, 128) or query.shape[2] == 0: 

159 return False 

160 if ratio and not 0 <= topk <= 512: 

161 return False 

162 return True 

163 

164 

165@lru_cache(maxsize=128) 

166@torch.inference_mode(False) 

167def _attention_metadata( 

168 device: torch.device, batch: int, length: int, heads: int, dim: int, 

169 raw_length: int, main_length: int, topk: int, ratio: int, residual: int, window: int, 

170) -> tuple: 

171 """Reuse shape metadata across evaluation and gradient-tracked training. 

172 

173 Evaluation may populate the cache under inference_mode. The backward 

174 saves the cached residual, so it must be an ordinary tensor regardless 

175 of which mode first populated the cache. 

176 """ 

177 # The training backward requires a zero residual tensor even at ratio one; 

178 # the forward interface instead omits that optional input at ratio one. 

179 residue = torch.full((batch,), residual, dtype=torch.int32, device=device) if main_length else None 

180 _, _, metadata_op = _load_attention_ops() 

181 metadata = metadata_op( 

182 heads, 1, dim, cmp_residual_kv=residue if ratio > 1 else None, batch_size=batch, max_seqlen_q=length, 

183 max_seqlen_ori_kv=raw_length, max_seqlen_cmp_kv=main_length, ori_topk=0, cmp_topk=topk, 

184 cmp_ratio=ratio, ori_mask_mode=4, cmp_mask_mode=3, ori_win_left=window-1, ori_win_right=0, 

185 layout_q="BSND", layout_kv="BSND", has_ori_kv=True, has_cmp_kv=bool(main_length), 

186 ) 

187 return residue, metadata 

188 

189 

190class _SparseFlashMla(torch.autograd.Function): 

191 """Wrap SparseFlashMla and SparseFlashMlaGrad without substituting the KL teacher.""" 

192 

193 @staticmethod 

194 def forward( 

195 ctx: Any, query: torch.Tensor, raw: torch.Tensor, main: torch.Tensor | None, 

196 indices: torch.Tensor | None, sinks: torch.Tensor, scale: float, 

197 ratio: int, residual: int, window: int, 

198 ) -> torch.Tensor: 

199 """Save the native output and LSE required by the training backward.""" 

200 batch, length, heads, dim = query.shape 

201 residue, metadata = _attention_metadata( 

202 query.device, batch, length, heads, dim, raw.shape[1], 0 if main is None else main.shape[1], 

203 0 if indices is None else indices.shape[-1], ratio, residual, window, 

204 ) 

205 options = {"softmax_scale": scale, "cmp_ratio": ratio, "ori_mask_mode": 4, "cmp_mask_mode": 3, 

206 "ori_win_left": window-1, "ori_win_right": 0, "layout_q": "BSND", "layout_kv": "BSND"} 

207 attention, _, _ = _load_attention_ops() 

208 output, lse = attention( 

209 query, ori_kv=raw, cmp_kv=main, cmp_sparse_indices=indices, sinks=sinks, 

210 cmp_residual_kv=residue if ratio > 1 else None, metadata=metadata, return_softmax_lse=True, **options, 

211 ) 

212 ctx.save_for_backward(query, raw, main, indices, sinks, residue, output, lse) 

213 ctx.options = options 

214 return output 

215 

216 @staticmethod 

217 def backward(ctx: Any, grad_output: torch.Tensor) -> tuple: 

218 """Return main-branch gradients without substituting the KL teacher.""" 

219 query, raw, main, indices, sinks, residue, output, lse = ctx.saved_tensors 

220 _, attention_grad, _ = _load_attention_ops() 

221 grad_query, grad_raw, grad_main, grad_sinks, _, _ = attention_grad( 

222 query, grad_output.contiguous(), output, lse, ori_kv=raw, cmp_kv=main, 

223 cmp_sparse_indices=indices, sinks=sinks, cmp_residual_kv=residue, **ctx.options, 

224 ) 

225 return grad_query, grad_raw, grad_main if main is not None else None, None, grad_sinks, None, None, None, None 

226 

227 

228def _bsnd_sparse_flash_mla( 

229 query: torch.Tensor, raw_key: torch.Tensor, main_key: torch.Tensor | None, 

230 indices: torch.Tensor | None, sinks: torch.Tensor, scale: float, ratio: int, 

231 window: int, query_offset: int, 

232 segment: tuple[int, int, int, int, int], raw_key_offset: int = 0, main_key_offset: int = 0, 

233 raw_document_starts: tuple = (), 

234) -> torch.Tensor: 

235 """Run one document segment per batch item with independently routed KV banks. 

236 

237 The operator owns Q tiling within this BSND call. Multiple packed 

238 documents use the TND entry with cumulative document lengths. 

239 

240 Args: 

241 query: Local attention Q in [B, H, Q, D]. 

242 raw_key: Full or halo raw KV in [B, 1, K, D]. 

243 main_key: Full or prefix compressed KV in [B, Kc, D], or None for SWA only. 

244 indices: Global compressed selection [B, Q, topk], invalid slots -1. 

245 sinks: One scalar logit per head. 

246 scale: Attention dot-product scale. 

247 ratio: Compression ratio, zero denotes the SWA-only layer. 

248 window: Number of raw tokens visible to each query including itself. 

249 query_offset: Global position of the first local query. 

250 segment: Local Q start/end, compressed document start/prefix end and residual, 

251 shared by all physical batch items. 

252 

253 Returns: 

254 Attention output in [B, Q, H, D]. 

255 """ 

256 if query.dtype != torch.bfloat16 or query.shape[-1] != 512 or window != 128: 

257 raise ValueError("V4 attention requires BF16, head_dim=512, and window=128") 

258 start, end, key_start, key_end, residual = segment 

259 document_start = dict(raw_document_starts).get(start, key_start * max(ratio, 1)) 

260 raw_left = max(document_start, query_offset + start - window + 1) 

261 raw_right = query_offset + end 

262 # contiguous() may keep stale singleton strides after slicing a BSND KV bank. 

263 batch, _, _, dim = query.shape 

264 raw = raw_key[:, :, raw_left-raw_key_offset:raw_right-raw_key_offset] 

265 raw = raw.squeeze(1).contiguous().view(batch, raw_right-raw_left, 1, dim) 

266 main = None 

267 selected = None 

268 if ratio and key_end > key_start: 

269 main = main_key[:, key_start-main_key_offset:key_end-main_key_offset] 

270 main = main.contiguous().view(batch, key_end-key_start, 1, dim) 

271 original = indices[:, start:end] 

272 selected = torch.where(original >= 0, original-key_start, -1).int() 

273 # Backward infers the active count from this width and causality; only KL requires 512 slots. 

274 selected = selected.unsqueeze(2).contiguous() 

275 return _SparseFlashMla.apply( 

276 query[:, :, start:end].transpose(1, 2).contiguous(), raw, main, selected, sinks.float(), 

277 scale, max(ratio, 1) if main is not None else 1, residual if main is not None else 0, window, 

278 ) 

279 

280 

281@lru_cache(maxsize=128) 

282@torch.inference_mode(False) 

283def _tnd_attention_metadata(plan, device, heads, dim, raw_ranges, ratio, window, has_keys, topk): 

284 """Describe independent raw and compressed coordinate systems to SMLA.""" 

285 cu_q, cu_k, residuals, _ = plan.lengths(device) 

286 raw_lengths = tuple(end-start for start, end in raw_ranges) 

287 cu_raw = torch.tensor((0, *accumulate(raw_lengths)), device=device, dtype=torch.int32) 

288 _, _, metadata_op = _load_attention_ops() 

289 options = {"layout_q": "TND", "layout_kv": "TND", "cmp_ratio": ratio, "ori_mask_mode": 4, 

290 "cmp_mask_mode": 3, "ori_win_left": window-1, "ori_win_right": 0} 

291 metadata = metadata_op( 

292 heads, 1, dim, cu_seqlens_q=cu_q, cu_seqlens_ori_kv=cu_raw, 

293 cu_seqlens_cmp_kv=cu_k if has_keys else None, 

294 cmp_residual_kv=residuals if has_keys and ratio > 1 else None, 

295 batch_size=len(plan.segments), max_seqlen_q=max(plan.q_lengths), 

296 max_seqlen_ori_kv=max(raw_lengths), max_seqlen_cmp_kv=max(plan.k_lengths) if has_keys else 0, 

297 ori_topk=0, cmp_topk=topk, has_ori_kv=True, has_cmp_kv=has_keys, **options, 

298 ) 

299 return cu_q, cu_raw, cu_k if has_keys else None, residuals if has_keys else None, metadata 

300 

301 

302class _SparseFlashMlaTND(torch.autograd.Function): 

303 """Native multi-document forward/backward; save all sequence tensors through autograd.""" 

304 

305 @staticmethod 

306 def forward( 

307 ctx: Any, query: torch.Tensor, raw: torch.Tensor, main: torch.Tensor | None, 

308 indices: torch.Tensor | None, sinks: torch.Tensor, scale: float, ratio: int, window: int, 

309 plan: CSATNDPlan, raw_ranges: tuple, 

310 ) -> torch.Tensor: 

311 """Save native output/LSE and independent raw/compressed cumulative lengths.""" 

312 cu_q, cu_raw, cu_k, residuals, metadata = _tnd_attention_metadata( 

313 plan, query.device, query.shape[1], query.shape[2], raw_ranges, ratio, window, main is not None, 

314 0 if indices is None else indices.shape[-1], 

315 ) 

316 options = {"layout_q": "TND", "layout_kv": "TND", "softmax_scale": scale, "cmp_ratio": ratio, 

317 "ori_mask_mode": 4, "cmp_mask_mode": 3, "ori_win_left": window-1, "ori_win_right": 0} 

318 forward, _, _ = _load_attention_ops() 

319 output, lse = forward( 

320 query, ori_kv=raw, cmp_kv=main, cmp_sparse_indices=indices, sinks=sinks, 

321 cu_seqlens_q=cu_q, cu_seqlens_ori_kv=cu_raw, cu_seqlens_cmp_kv=cu_k, 

322 cmp_residual_kv=residuals if ratio > 1 else None, metadata=metadata, 

323 return_softmax_lse=True, **options, 

324 ) 

325 ctx.save_for_backward(query, raw, main, indices, sinks, output, lse, cu_q, cu_raw, cu_k, residuals) 

326 ctx.options = options 

327 return output 

328 

329 @staticmethod 

330 def backward(ctx: Any, grad_output: torch.Tensor) -> tuple: 

331 """Return each physical bank's gradient to its autograd communication owner.""" 

332 query, raw, main, indices, sinks, output, lse, cu_q, cu_raw, cu_k, residuals = ctx.saved_tensors 

333 _, backward, _ = _load_attention_ops() 

334 dq, dr, dm, ds, _, _ = backward( 

335 query, grad_output.contiguous(), output, lse, ori_kv=raw, cmp_kv=main, 

336 cmp_sparse_indices=indices, sinks=sinks, cu_seqlens_q=cu_q, cu_seqlens_ori_kv=cu_raw, 

337 cu_seqlens_cmp_kv=cu_k, cmp_residual_kv=residuals, **ctx.options, 

338 ) 

339 return dq, dr, dm if main is not None else None, None, ds, None, None, None, None, None 

340 

341 

342def sparse_flash_mla( 

343 query: torch.Tensor, raw_key: torch.Tensor, main_key: torch.Tensor | None, 

344 indices: torch.Tensor | None, sinks: torch.Tensor, scale: float, ratio: int, 

345 window: int, query_offset: int, segments: tuple, raw_key_offset: int = 0, main_key_offset: int = 0, 

346 raw_document_starts: tuple = (), 

347) -> torch.Tensor: 

348 """Batch packed documents in native TND with independently routed KV banks. 

349 

350 Args: 

351 query: Local attention Q [batch, heads, queries, 512]. 

352 raw_key: Full or halo raw KV [batch, 1, keys, 512]. 

353 main_key: Full or document-prefix compressed KV [batch, compressed keys, 512], or None. 

354 indices: Sorted global compressed key IDs, invalid slots -1. 

355 sinks: Per-head scalar sink logits. 

356 scale: Attention dot-product scale. 

357 ratio: Compression ratio; zero selects pure SWA. 

358 window: Raw sliding-window size, including the current token. 

359 query_offset: Global position of the first local query. 

360 segments: Local Q start/end, compressed document start/prefix end and residual. 

361 raw_document_starts: Explicit (local Q start, raw document start) pairs for natural lengths. 

362 raw_key_offset: Global position of the first row in the compact raw KV bank. 

363 main_key_offset: Global compressed ID represented by row zero of main_key. 

364 

365 Returns: 

366 Attention output [batch, queries, heads, 512]. A single document or 

367 ordinary physical batch uses BSND. Empty compressed prefixes form a 

368 separate SWA-only TND group without inventing compressed keys. 

369 """ 

370 if len(segments) == 1: 

371 return _bsnd_sparse_flash_mla( 

372 query, raw_key, main_key, indices, sinks, scale, ratio, window, query_offset, segments[0], 

373 raw_key_offset, main_key_offset, raw_document_starts, 

374 ) 

375 if query.shape[0] != 1: 

376 raise ValueError("Packed Sparse Flash MLA requires batch size one") 

377 if query.dtype != torch.bfloat16 or query.shape[-1] != 512 or window != 128: 

378 raise ValueError("V4 attention requires BF16, head_dim=512, and window=128") 

379 document_starts = dict(raw_document_starts) 

380 output = None 

381 for has_keys in (True, False): 

382 plan = csa_tnd_plan(segments, ratio, has_keys) 

383 if plan is None: 

384 continue 

385 raw_ranges = tuple( 

386 (max(document_starts.get(s, ks * max(ratio, 1)), query_offset+s-window+1)-raw_key_offset, 

387 query_offset+e-raw_key_offset) 

388 for s, e, ks, _, _ in plan.segments 

389 ) 

390 raw = pack_ranges(raw_key.squeeze(0).squeeze(0), raw_ranges).unsqueeze(1) 

391 main = plan.key(main_key, main_key_offset).unsqueeze(1) if has_keys else None 

392 selected = plan.local_indices(indices) if has_keys else None 

393 part = _SparseFlashMlaTND.apply( 

394 plan.query(query.transpose(1, 2)), raw, main, selected, sinks.float(), 

395 scale, max(ratio, 1) if has_keys else 1, window, plan, raw_ranges, 

396 ) 

397 # A complete Q partition is already ordered; a canvas would retain another full output. 

398 if plan.covers_queries(query.shape[2]): 

399 return part.unsqueeze(0) 

400 if output is None: 

401 output = torch.zeros_like(query.transpose(1, 2)) 

402 plan.restore(part, output) 

403 return torch.zeros_like(query.transpose(1, 2)) if output is None else output