Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / distributed / context_parallel / attention.py: 52%

145 statements  

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

1# Copyright 2025-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 

16"""context_parallel.attention: CP attention-layout adaptations. 

17 

18Model-side CP adaptations on top of the collectives in 

19context_parallel/collectives.py: MoME halo exchange, MLA/DSA all-to-all 

20layouts, head/tail load-balanced attention and the D-04 offset-aware 

21causal mask. 

22 

23Split out of components/distributed/cp_utils.py in stage 4e. 

24""" 

25 

26import contextvars 

27import functools 

28import inspect 

29from dataclasses import dataclass 

30from typing import Any, Callable, Optional 

31import torch 

32import torch.distributed as dist 

33from torch import Tensor 

34from torch.distributed.nn.functional import all_gather as differentiable_all_gather 

35from hyper_parallel.core.utils import communication 

36from hyper_parallel.distributed.context_parallel.collectives import ( 

37 _ULYSSES_WRAPPED_FLAG, 

38 _gather_sequence, 

39 _global_seq_len, 

40 _head_to_sequence, 

41 _sequence_to_head, 

42 _slice_sink, 

43 flex_cp_allgather, 

44) 

45 

46 

47@dataclass 

48class _DSATensorContext: 

49 query: Tensor 

50 key: Tensor 

51 q_pe: Tensor 

52 k_pe: Tensor 

53 

54 

55_dsa_tensor_context = contextvars.ContextVar( 

56 "hyper_dsa_tensor_context", default=None) 

57 

58_DSA_KL_ARGUMENTS = ( 

59 "index_query", "index_key", "merge_weight", "query", "key", 

60 "topk_indices", "softmax_max", "softmax_sum", "query_rope", 

61 "key_rope", "actual_seq_qlen", "actual_seq_klen", "scale", 

62 "loss_coeff", 

63) 

64_DSA_KL_SIGNATURE = inspect.Signature( 

65 inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD) 

66 for name in _DSA_KL_ARGUMENTS 

67) 

68 

69 

70def _mome_cp_halo_exchange(attention_module, context): 

71 """Configure cross-rank halo exchange for MoME convolution.""" 

72 original = getattr(attention_module, "_apply_mome") 

73 if getattr(original, _ULYSSES_WRAPPED_FLAG, False): 

74 return 

75 

76 @functools.wraps(original) 

77 def apply_mome_with_halo( 

78 hidden_states: Tensor, 

79 mome_mask: Tensor, 

80 conv: Any, 

81 use_fused: bool, 

82 ) -> Tensor: 

83 """Run MoME convolution with the previous rank's halo rows prepended.""" 

84 halo = conv.kernel_size[0] - 1 

85 if halo == 0: 

86 return original(hidden_states, mome_mask, conv, use_fused) 

87 if hidden_states.size(1) < halo: 

88 raise ValueError( 

89 f"local sequence {hidden_states.size(1)} is shorter than " 

90 f"MOME halo {halo}") 

91 tails = differentiable_all_gather( 

92 hidden_states[:, -halo:].contiguous(), group=context.group) 

93 mask_tail = mome_mask[:, -halo:].to( 

94 hidden_states.dtype).contiguous() 

95 masks = differentiable_all_gather(mask_tail, group=context.group) 

96 if context.rank == 0: 

97 left_states = tails[-1] * 0 

98 left_mask = torch.zeros_like(mask_tail, dtype=torch.bool) 

99 else: 

100 left_states = tails[context.rank - 1] 

101 left_mask = masks[context.rank - 1].bool() 

102 output = original( 

103 torch.cat((left_states, hidden_states), dim=1), 

104 torch.cat((left_mask, mome_mask.bool()), dim=1), conv, use_fused) 

105 return output[:, halo:].contiguous() 

106 

107 setattr(apply_mome_with_halo, _ULYSSES_WRAPPED_FLAG, True) 

108 setattr(attention_module, "_apply_mome", apply_mome_with_halo) 

109 

110 

111def _mla_cp_alltoall(attention_functions, context): 

112 """Configure CP all-to-all around the MLA backend.""" 

113 original = attention_functions["npu_fa_rescale"] 

114 if getattr(original, _ULYSSES_WRAPPED_FLAG, False): 

115 return 

116 

117 @functools.wraps(original) 

118 def mla_with_sequence_head_exchange( 

119 module: Any, query: Tensor, key: Tensor, value: Tensor, 

120 attention_mask: Any, **kwargs: Any) -> Tensor: 

121 """Run the MLA backend with Q/K/V exchanged to head-sharded layout.""" 

122 if module.attention_type != "mla": 

123 return original( 

124 module, query, key, value, attention_mask, **kwargs) 

125 if not module.apply_fa_rescale or module.use_fused_sink_fa: 

126 raise ValueError( 

127 "MLA CP supports only non-fused npu_fa_rescale") 

128 local_shape = tuple(query.shape) 

129 query = _sequence_to_head(query, context) 

130 key = _sequence_to_head(key, context) 

131 value = _sequence_to_head(value, context) 

132 length = query.size(1) 

133 call_kwargs = kwargs.copy() 

134 call_kwargs.update( 

135 seq_length=length, 

136 n_head=query.size(2), 

137 actual_q_len=_global_seq_len( 

138 kwargs.get("actual_q_len"), length, query.device), 

139 actual_kv_len=_global_seq_len( 

140 kwargs.get("actual_kv_len"), length, query.device), 

141 param_sink_key=_slice_sink( 

142 kwargs.get("param_sink_key"), context), 

143 param_sink_value=_slice_sink( 

144 kwargs.get("param_sink_value"), context), 

145 ) 

146 output = _head_to_sequence( 

147 original( 

148 module, query, key, value, attention_mask, **call_kwargs), 

149 context) 

150 if output.shape[:3] != torch.Size(local_shape[:3]): 

151 raise RuntimeError( 

152 f"MLA CP output {tuple(output.shape)} does not restore " 

153 f"{local_shape}") 

154 return output 

155 

156 setattr(mla_with_sequence_head_exchange, _ULYSSES_WRAPPED_FLAG, True) 

157 attention_functions["npu_fa_rescale"] = mla_with_sequence_head_exchange 

158 

159 

160def _gather_dsa_teacher_heads(tensor: Tensor, head_dim: int, context: Any) -> Tensor: 

161 """Restore all teacher heads without adding an auxiliary gradient path.""" 

162 return torch.cat( 

163 differentiable_all_gather(tensor.detach().contiguous(), group=context.group), dim=head_dim) 

164 

165 

166def _dsa_cp_alltoall(attention_module, attention_functions, context): 

167 """Configure CP all-to-all for the DSA indexer, attention, and KL loss.""" 

168 original_indexer = attention_module.dsa_lightning_indexer_forward 

169 original_sparse = attention_functions["dsa_sparse_attention"] 

170 original_kl = attention_module.SparseLightningIndexerKLLossTrainFunction 

171 

172 if not getattr(original_indexer, _ULYSSES_WRAPPED_FLAG, False): 

173 @functools.wraps(original_indexer) 

174 def index_with_gathered_sequence( 

175 module: Any, index_query: Tensor, index_key: Tensor, 

176 merge_weight: Tensor, 

177 actual_q_len: Any, actual_kv_len: Any) -> Any: 

178 """Keep the full head reduction before the non-linear top-k.""" 

179 index_query = _gather_sequence(index_query, context) 

180 merge_weight = _gather_sequence(merge_weight, context) 

181 index_key = _gather_sequence(index_key, context) 

182 length = index_query.size(1) 

183 return original_indexer( 

184 module, index_query, index_key, merge_weight, 

185 _global_seq_len( 

186 actual_q_len, length, index_query.device), 

187 _global_seq_len( 

188 actual_kv_len, length, index_query.device)) 

189 

190 setattr(index_with_gathered_sequence, _ULYSSES_WRAPPED_FLAG, True) 

191 attention_module.dsa_lightning_indexer_forward = ( 

192 index_with_gathered_sequence) 

193 

194 if not getattr(original_sparse, _ULYSSES_WRAPPED_FLAG, False): 

195 @functools.wraps(original_sparse) 

196 def sparse_attention_with_gathered_kv( 

197 module: Any, query: Tensor, key: Tensor, value: Tensor, 

198 attention_mask: Any, **kwargs: Any) -> tuple[Tensor, Any, Any]: 

199 """Run DSA sparse attention with head-sharded Q and gathered K/V.""" 

200 del attention_mask 

201 local_shape = tuple(query.shape) 

202 query = _sequence_to_head(query, context) 

203 q_pe = _sequence_to_head(kwargs["q_pe"], context) 

204 key = _gather_sequence(key, context) 

205 value = _gather_sequence(value, context) 

206 k_pe = _gather_sequence(kwargs["k_pe"], context) 

207 length = query.size(1) 

208 call_kwargs = kwargs.copy() 

209 call_kwargs.update( 

210 q_pe=q_pe, k_pe=k_pe, seq_length=length, 

211 n_head=query.size(2), 

212 actual_q_len=_global_seq_len( 

213 kwargs.get("actual_q_len"), length, query.device), 

214 actual_kv_len=_global_seq_len( 

215 kwargs.get("actual_kv_len"), length, query.device), 

216 ) 

217 old_heads = module.num_heads 

218 module.num_heads = query.size(2) 

219 try: 

220 output, softmax_max, softmax_sum = original_sparse( 

221 module, query, key, value, None, **call_kwargs) 

222 finally: 

223 module.num_heads = old_heads 

224 if module.training and not module.freeze_dsa: 

225 _dsa_tensor_context.set(_DSATensorContext( 

226 query=query, key=key, q_pe=q_pe, k_pe=k_pe)) 

227 output = _head_to_sequence(output, context) 

228 if output.shape[:3] != torch.Size(local_shape[:3]): 

229 raise RuntimeError( 

230 f"DSA CP output {tuple(output.shape)} does not restore " 

231 f"{local_shape}") 

232 return output, softmax_max, softmax_sum 

233 

234 setattr(sparse_attention_with_gathered_kv, _ULYSSES_WRAPPED_FLAG, True) 

235 attention_functions["dsa_sparse_attention"] = ( 

236 sparse_attention_with_gathered_kv) 

237 

238 if not getattr(original_kl, _ULYSSES_WRAPPED_FLAG, False): 

239 class CPDSAKLLoss: 

240 """Proxy the DSA KL loss with CP-transformed attention inputs.""" 

241 

242 @staticmethod 

243 def apply(*args: Any, **kwargs: Any) -> Any: 

244 """Apply the original KL loss with saved global sequence tensors.""" 

245 arguments = _DSA_KL_SIGNATURE.bind(*args, **kwargs).arguments 

246 saved = _dsa_tensor_context.get() 

247 if saved is not None: 

248 _dsa_tensor_context.set(None) 

249 length = saved.query.size(1) 

250 arguments.update( 

251 query=_gather_dsa_teacher_heads(saved.query, 2, context).flatten(0, 1), 

252 key=saved.key.flatten(0, 1), 

253 query_rope=_gather_dsa_teacher_heads(saved.q_pe, 2, context).flatten(0, 1), 

254 key_rope=saved.k_pe.flatten(0, 1), 

255 softmax_max=_gather_dsa_teacher_heads(arguments["softmax_max"], -1, context), 

256 softmax_sum=_gather_dsa_teacher_heads(arguments["softmax_sum"], -1, context), 

257 actual_seq_qlen=_global_seq_len( 

258 arguments["actual_seq_qlen"], length, saved.query.device), 

259 actual_seq_klen=_global_seq_len( 

260 arguments["actual_seq_klen"], length, saved.query.device), 

261 ) 

262 return original_kl.apply( 

263 *(arguments[name] for name in _DSA_KL_ARGUMENTS)) 

264 

265 setattr(CPDSAKLLoss, _ULYSSES_WRAPPED_FLAG, True) 

266 attention_module.SparseLightningIndexerKLLossTrainFunction = ( 

267 CPDSAKLLoss) 

268 

269 

270def _head_tail_peer_rank(cp_mesh: Any) -> int: 

271 """Return the mirror rank in the CP mesh.""" 

272 rank_list = list(cp_mesh.rank_list) 

273 return rank_list[cp_mesh.size() - 1 - rank_list.index(dist.get_rank())] 

274 

275 

276def _run_head_tail_half( 

277 attention_fn: Callable[[Tensor, Tensor, Tensor, dict[str, Any]], Any], 

278 query: Tensor, key: Tensor, value: Tensor, 

279 call_kwargs: dict[str, Any]) -> Tensor: 

280 """Run attention for one query half and check its output contract.""" 

281 output = attention_fn(query, key, value, call_kwargs) 

282 if not isinstance(output, Tensor): 

283 raise TypeError( 

284 "Head-Tail load balance requires the attention callable to " 

285 f"return a Tensor, got {type(output).__name__}" 

286 ) 

287 return output 

288 

289 

290def head_tail_load_balance_attention( 

291 attention_fn: Callable[[Tensor, Tensor, Tensor, dict[str, Any]], Any], 

292 query: Tensor, key: Tensor, value: Tensor, 

293 attention_kwargs: dict[str, Any], cp_mesh: Any, *, 

294 peer_attention_kwargs: Optional[dict[str, Any]] = None) -> Any: 

295 """Run local-tensor Colossal Head-Tail communication. 

296 

297 The caller prepares split-specific mask or position metadata. When 

298 ``peer_attention_kwargs`` is omitted, both attention calls reuse 

299 ``attention_kwargs`` for backward compatibility. 

300 """ 

301 if cp_mesh is None or cp_mesh.size() <= 1: 

302 raise ValueError("Head-Tail load balance requires an active CP mesh") 

303 local_q_len = query.shape[2] 

304 if local_q_len % 2: 

305 raise ValueError( 

306 "Head-Tail load balance requires an even local Q sequence " 

307 f"length, got {local_q_len}; pad the global sequence to a " 

308 f"multiple of 2 * cp_size ({2 * cp_mesh.size()})" 

309 ) 

310 

311 peer_rank = _head_tail_peer_rank(cp_mesh) 

312 query_peer = communication.p2p_exchange( 

313 query.narrow(2, local_q_len // 2, local_q_len // 2), peer_rank) 

314 global_key, global_value = flex_cp_allgather(key, value, 2, cp_mesh) 

315 keep_output = _run_head_tail_half( 

316 attention_fn, query.narrow(2, 0, local_q_len // 2), 

317 global_key, global_value, attention_kwargs) 

318 peer_output = _run_head_tail_half( 

319 attention_fn, query_peer, global_key, global_value, 

320 attention_kwargs if peer_attention_kwargs is None else peer_attention_kwargs, 

321 ) 

322 return torch.cat( 

323 [keep_output, communication.p2p_exchange(peer_output, peer_rank)], dim=2) 

324 

325 

326def _cp_offset_causal_mask(q_len: int, kv_len: int, lo: int, 

327 device, dtype=torch.bool): 

328 """D-04: offset-aware causal mask (this rank's Q chunk has global offset lo). 

329 

330 Attendable positions: j <= lo + i (i is the local Q row index). 

331 Replaces is_causal=True -- torch SDPA's is_causal is top-left aligned when 

332 q_len != kv_len (equivalent to assuming Q starts at global position 0), so 

333 under CP the chunks of rank>0 would be incorrectly masked (G4). 

334 """ 

335 i = torch.arange(q_len, device=device).view(-1, 1) 

336 j = torch.arange(kv_len, device=device).view(1, -1) 

337 return (j <= (lo + i)).to(dtype)