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

166 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-25 05:45 +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) -> torch.Tensor: 

61 """Pack complete visible document prefixes from a [1,K,...] bank.""" 

62 return pack_ranges(tensor.squeeze(0), self.k_ranges) 

63 

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

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

66 return _device_lengths(self, device) 

67 

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

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

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

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

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

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

74 

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

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

77 selected = self.query(indices) 

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

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

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

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

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

83 

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

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

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

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

88 

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

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

91 cursor = 0 

92 for start, end in self.q_ranges: 

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

94 cursor += end-start 

95 

96 

97@lru_cache(maxsize=128) 

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

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

100 active = tuple(item for item in segments 

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

102 if not active: 

103 return None 

104 return CSATNDPlan( 

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

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

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

108 ) 

109 

110 

111@lru_cache(maxsize=128) 

112@torch.inference_mode(False) 

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

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

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

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

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

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

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

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

121 return cu_q, cu_k, residuals, offsets 

122 

123 

124@lru_cache(maxsize=1) 

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

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

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

128 try: 

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

130 sparse_flash_mla as sparse_flash_mla_op, 

131 sparse_flash_mla_grad, sparse_flash_mla_metadata, 

132 ) 

133 except ImportError as err: 

134 raise RuntimeError( 

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

136 "and sparse_flash_mla_metadata. " 

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

138 "environment setup and tested versions." 

139 ) from err 

140 return sparse_flash_mla_op, sparse_flash_mla_grad, sparse_flash_mla_metadata 

141 

142 

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

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

145 

146 Args: 

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

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

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

150 topk: Logical compressed selection width, before padding. 

151 """ 

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

153 return False 

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

155 return False 

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

157 return False 

158 if ratio and not 1 <= topk <= 512: 

159 return False 

160 return True 

161 

162 

163@lru_cache(maxsize=128) 

164@torch.inference_mode(False) 

165def _attention_metadata( 

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

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

168) -> tuple: 

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

170 

171 Evaluation may populate the cache under inference_mode. The backward 

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

173 of which mode first populated the cache. 

174 """ 

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

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

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

178 _, _, metadata_op = _load_attention_ops() 

179 metadata = metadata_op( 

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

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

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

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

184 ) 

185 return residue, metadata 

186 

187 

188class _SparseFlashMla(torch.autograd.Function): 

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

190 

191 @staticmethod 

192 def forward( 

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

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

195 ratio: int, residual: int, window: int, 

196 ) -> torch.Tensor: 

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

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

199 residue, metadata = _attention_metadata( 

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

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

202 ) 

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

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

205 attention, _, _ = _load_attention_ops() 

206 output, lse = attention( 

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

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

209 ) 

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

211 ctx.options = options 

212 return output 

213 

214 @staticmethod 

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

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

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

218 _, attention_grad, _ = _load_attention_ops() 

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

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

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

222 ) 

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

224 

225 

226def _bsnd_sparse_flash_mla( 

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

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

229 window: int, query_offset: int, 

230 segment: tuple[int, int, int, int, int], 

231) -> torch.Tensor: 

232 """Run one document segment per batch item using the original all-gather banks. 

233 

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

235 documents use the TND entry with cumulative document lengths. 

236 

237 Args: 

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

239 raw_key: Global raw KV in [B, 1, K, D]. 

240 main_key: Global compressed KV in [B, Kc, D], or None for SWA only. 

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

242 sinks: One scalar logit per head. 

243 scale: Attention dot-product scale. 

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

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

246 query_offset: Global position of the first local query. 

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

248 shared by all physical batch items. 

249 

250 Returns: 

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

252 """ 

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

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

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

256 document_start = key_start * max(ratio, 1) 

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

258 raw_right = query_offset + end 

259 raw = raw_key[:, :, raw_left:raw_right].transpose(1, 2).contiguous() 

260 main = None 

261 selected = None 

262 if ratio and key_end > key_start: 

263 main = main_key[:, key_start:key_end].unsqueeze(2).contiguous() 

264 original = indices[:, start:end] 

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

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

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

268 return _SparseFlashMla.apply( 

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

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

271 ) 

272 

273 

274@lru_cache(maxsize=128) 

275@torch.inference_mode(False) 

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

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

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

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

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

281 _, _, metadata_op = _load_attention_ops() 

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

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

284 metadata = metadata_op( 

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

286 cu_seqlens_cmp_kv=cu_k if has_keys else None, 

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

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

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

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

291 ) 

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

293 

294 

295class _SparseFlashMlaTND(torch.autograd.Function): 

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

297 

298 @staticmethod 

299 def forward( 

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

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

302 plan: CSATNDPlan, raw_ranges: tuple, 

303 ) -> torch.Tensor: 

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

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

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

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

308 ) 

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

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

311 forward, _, _ = _load_attention_ops() 

312 output, lse = forward( 

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

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

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

316 return_softmax_lse=True, **options, 

317 ) 

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

319 ctx.options = options 

320 return output 

321 

322 @staticmethod 

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

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

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

326 _, backward, _ = _load_attention_ops() 

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

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

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

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

331 ) 

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

333 

334 

335def sparse_flash_mla( 

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

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

338 window: int, query_offset: int, segments: tuple, 

339) -> torch.Tensor: 

340 """Batch packed documents in native TND while preserving the all-gather banks. 

341 

342 Args: 

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

344 raw_key: Global raw KV [batch, 1, keys, 512]. 

345 main_key: Global compressed KV [batch, compressed keys, 512], or None. 

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

347 sinks: Per-head scalar sink logits. 

348 scale: Attention dot-product scale. 

349 ratio: Compression ratio; zero selects pure SWA. 

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

351 query_offset: Global position of the first local query. 

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

353 

354 Returns: 

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

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

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

358 """ 

359 if len(segments) == 1: 

360 return _bsnd_sparse_flash_mla( 

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

362 ) 

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

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

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

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

367 output = None 

368 for has_keys in (True, False): 

369 plan = csa_tnd_plan(segments, ratio, has_keys) 

370 if plan is None: 

371 continue 

372 raw_ranges = tuple( 

373 (max(ks * max(ratio, 1), query_offset+s-window+1), query_offset+e) 

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

375 ) 

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

377 main = plan.key(main_key).unsqueeze(1) if has_keys else None 

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

379 part = _SparseFlashMlaTND.apply( 

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

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

382 ) 

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

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

385 return part.unsqueeze(0) 

386 if output is None: 

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

388 plan.restore(part, output) 

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