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

124 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"""Document-local compression blocks and compact bank ownership for contiguous CP.""" 

16 

17from __future__ import annotations 

18 

19from bisect import bisect_right 

20from dataclasses import dataclass 

21from functools import lru_cache 

22from itertools import accumulate 

23from typing import Any 

24 

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

26from torch.nn import functional # pylint: disable=forbidden-backend-import 

27 

28 

29@dataclass(frozen=True) 

30class CompressionBoundaryRoute: 

31 """Projected rows needed to complete a block owned by its final-token rank.""" 

32 

33 global_start: int 

34 send_offsets: tuple[int, ...] 

35 input_splits: tuple[int, ...] 

36 output_splits: tuple[int, ...] 

37 needs_exchange: bool 

38 

39 

40def _boundary_route(local_length: int, rank: int, intervals: tuple) -> CompressionBoundaryRoute: 

41 """Derive left-boundary exchanges; all ranks enter the same collective.""" 

42 local_start = rank * local_length 

43 global_start, global_end = intervals[rank] 

44 offsets, sends, receives = [], [], [] 

45 for peer, (start, end) in enumerate(intervals): 

46 first = max(local_start, start) 

47 count = max(0, min(local_start + local_length, end) - first) if peer != rank else 0 

48 offsets.append(first - local_start if count else 0) 

49 sends.append(count) 

50 peer_start = peer * local_length 

51 receives.append(max(0, min(peer_start + local_length, global_end) - max(peer_start, global_start)) 

52 if peer != rank else 0) 

53 needs_exchange = any(start < peer * local_length for peer, (start, _) in enumerate(intervals)) 

54 return CompressionBoundaryRoute(global_start, tuple(offsets), tuple(sends), tuple(receives), needs_exchange) 

55 

56 

57@lru_cache(maxsize=128) 

58def compressed_document_offsets(boundaries: tuple[int, ...], ratio: int) -> tuple[int, ...]: 

59 """Return canonical K boundaries, dropping only incomplete document tails. 

60 

61 Args: 

62 boundaries: Cumulative raw document boundaries, beginning at zero. 

63 ratio: Positive number of raw tokens per compressed block. 

64 

65 Returns: 

66 Cumulative complete-block counts in document order, beginning at zero. 

67 """ 

68 if ratio < 1 or len(boundaries) < 2 or boundaries[0] != 0 or any( 

69 end <= start for start, end in zip(boundaries, boundaries[1:])): 

70 raise ValueError("Compression requires a positive ratio and strictly increasing document boundaries") 

71 return (0, *accumulate((end-start)//ratio for start, end in zip(boundaries, boundaries[1:]))) 

72 

73 

74@lru_cache(maxsize=128) 

75def compressed_owner_offsets(boundaries: tuple[int, ...], ratio: int, local_length: int) -> tuple[int, ...]: 

76 """Assign each complete block to the raw rank containing its last token. 

77 

78 Args: 

79 boundaries: Cumulative raw document boundaries, beginning at zero. 

80 ratio: Positive number of raw tokens per compressed block. 

81 local_length: Equal positive raw-token count on each CP rank. 

82 

83 Returns: 

84 Cumulative compressed-bank offsets for each rank and the final bank end. 

85 """ 

86 keys = compressed_document_offsets(boundaries, ratio) 

87 if local_length <= 0 or boundaries[-1] % local_length: 

88 raise ValueError("Compression requires equal positive raw CP shard lengths") 

89 offsets = [] 

90 for position in range(0, boundaries[-1], local_length): 

91 document = bisect_right(boundaries, position) - 1 

92 offsets.append(keys[document] + (position-boundaries[document])//ratio) 

93 return (*offsets, keys[-1]) 

94 

95 

96@dataclass(frozen=True) 

97class DocumentCompressionPlan: 

98 """Immutable block ranges; never enumerate the full sequence on the CPU.""" 

99 

100 ratio: int 

101 local_length: int 

102 rank: int 

103 counts: tuple[int, ...] 

104 local_blocks: tuple[tuple[int, int], ...] 

105 route: CompressionBoundaryRoute 

106 

107 def indices(self, device: torch.device) -> tuple[torch.Tensor, torch.Tensor]: 

108 """Reuse device indices for local blocks and their original rotary positions. 

109 

110 Args: 

111 device: Device on which the row indices are consumed. 

112 

113 Returns: 

114 Flattened pooling rows and block-start rows, relative to exchanged input. 

115 """ 

116 return _compression_indices(self, device) 

117 

118 def exchange(self, tensor: torch.Tensor, context: Any) -> torch.Tensor: 

119 """Complete boundary blocks with the existing differentiable A2AV protocol. 

120 

121 Args: 

122 tensor: Local projected values with raw tokens on dimension one. 

123 context: CP boundary-dispatch context, or None when no exchange is needed. 

124 

125 Returns: 

126 Required preceding rows followed by the local rows, preserving autograd. 

127 """ 

128 if not self.route.needs_exchange: 

129 return tensor 

130 if context is None or context.exchange_boundary is None: 

131 raise ValueError("natural document compression across CP requires boundary dispatch") 

132 return context.exchange_boundary(tensor, self.route) 

133 

134 def pool(self, key: torch.Tensor, gate: torch.Tensor, context: Any) -> torch.Tensor: 

135 """Pool complete same-document groups using the model's original FP32 softmax. 

136 

137 Args: 

138 key: Local projected values shaped [batch, raw tokens, channels]. 

139 gate: Pooling logits with the same shape as key. 

140 context: CP boundary-dispatch context, or None when no exchange is needed. 

141 

142 Returns: 

143 Pooled values shaped [batch, locally owned complete blocks, channels]. 

144 """ 

145 rows = self.indices(key.device)[0] 

146 projected = self.exchange(torch.cat((key, gate), dim=-1), context) 

147 blocks = projected.index_select(1, rows).reshape( 

148 key.shape[0], self.counts[self.rank], self.ratio, projected.shape[-1], 

149 ) 

150 values, scores = blocks.chunk(2, dim=-1) 

151 return (values * scores.float().softmax(dim=2).to(values.dtype)).sum(dim=2) 

152 

153 def positions(self, cos: torch.Tensor, sin: torch.Tensor, context: Any) -> tuple: 

154 """Take original block-start RoPE; retain FP32 boundary metadata precision. 

155 

156 Args: 

157 cos: Local raw-token rotary cosine coefficients. 

158 sin: Local raw-token rotary sine coefficients with the same shape as cos. 

159 context: CP boundary-dispatch context, or None when no exchange is needed. 

160 

161 Returns: 

162 Cosine and sine coefficients for each locally owned block's first token. 

163 """ 

164 positions = self.indices(cos.device)[1] 

165 embeddings = self.exchange(torch.cat((cos, sin), dim=-1), context) 

166 return embeddings.index_select(1, positions).chunk(2, dim=-1) 

167 

168 def launch_bank(self, tensor: torch.Tensor, context: Any) -> CompactBankGather: 

169 """All-gather equal-capacity containers and exclude every padding slot. 

170 

171 Args: 

172 tensor: Locally owned compressed bank with blocks on dimension one. 

173 context: CP context providing an asynchronous sequence all-gather. 

174 

175 Returns: 

176 Handle whose wait method returns the global bank without padding slots. 

177 """ 

178 capacity = max(self.counts) 

179 if not capacity: 

180 return CompactBankGather(None, tensor, None) 

181 padded = tensor if tensor.shape[1] == capacity else functional.pad( 

182 tensor, (0, 0, 0, capacity - tensor.shape[1]), 

183 ) 

184 slots = _bank_slots(self.counts, tensor.device) 

185 return CompactBankGather(context.launch(padded, 1), tensor, slots) 

186 

187 

188@dataclass 

189class CompactBankGather: 

190 """Complete one bank gather with at most one compacting index selection.""" 

191 

192 handle: Any 

193 tensor: torch.Tensor 

194 slots: torch.Tensor | None 

195 

196 def wait(self) -> torch.Tensor: 

197 """Keep compaction in autograd so all consumers return gradients to owners. 

198 

199 Returns: 

200 Complete compressed bank in canonical document order, without padding. 

201 """ 

202 output = self.tensor if self.handle is None else self.handle.wait() 

203 return output if self.slots is None else output.index_select(1, self.slots) 

204 

205 

206@lru_cache(maxsize=128) 

207def document_compression_plan(boundaries: tuple[int, ...], ratio: int, local_length: int, 

208 size: int, rank: int) -> DocumentCompressionPlan: 

209 """Assign complete blocks to the rank owning their final token. 

210 

211 Work on document/rank intersections rather than all global token rows. 

212 The packed-sequence boundary validator owns interval validation. 

213 

214 Args: 

215 boundaries: Cumulative raw document boundaries covering the global sequence. 

216 ratio: Number of raw tokens per compressed block, greater than one. 

217 local_length: Equal positive raw-token count on each CP rank. 

218 size: Number of contiguous CP shards. 

219 rank: Local rank within the CP group. 

220 

221 Returns: 

222 Complete-block ownership, local block ranges, and boundary-exchange routing. 

223 """ 

224 if ratio <= 1 or local_length <= 0 or not 0 <= rank < size: 

225 raise ValueError("document block planning requires ratio > 1 and a valid contiguous CP shard") 

226 if boundaries[0] != 0 or boundaries[-1] != size * local_length: 

227 raise ValueError("document boundaries must cover the complete contiguous CP sequence") 

228 owners = compressed_owner_offsets(boundaries, ratio, local_length) 

229 counts = tuple(end-start for start, end in zip(owners, owners[1:])) 

230 first_rows = [peer * local_length for peer in range(size)] 

231 local_blocks = [] 

232 for start, end in zip(boundaries, boundaries[1:]): 

233 blocks = (end - start) // ratio 

234 if not blocks: 

235 continue 

236 first_owner = (start + ratio - 1) // local_length 

237 last_owner = (start + blocks * ratio - 1) // local_length 

238 for peer in range(first_owner, last_owner + 1): 

239 first = max(0, (peer * local_length - start) // ratio) 

240 last = min(blocks, ((peer + 1) * local_length - start) // ratio) 

241 if last > first: 

242 first_rows[peer] = min(first_rows[peer], start + first * ratio) 

243 if peer == rank: 

244 local_blocks.append((start + first * ratio, last - first)) 

245 intervals = tuple((first, (peer + 1) * local_length) for peer, first in enumerate(first_rows)) 

246 return DocumentCompressionPlan(ratio, local_length, rank, tuple(counts), tuple(local_blocks), 

247 _boundary_route(local_length, rank, intervals)) 

248 

249 

250@lru_cache(maxsize=8) 

251@torch.inference_mode(False) 

252def _compression_indices(plan: DocumentCompressionPlan, device: torch.device) -> tuple: 

253 """Reuse block rows; bound the cache since compact AG maps scale with global K.""" 

254 starts = [torch.arange(start, start + count * plan.ratio, plan.ratio, device=device) 

255 for start, count in plan.local_blocks] 

256 starts = torch.cat(starts) if starts else torch.empty(0, device=device, dtype=torch.long) 

257 starts = starts - plan.route.global_start 

258 rows = (starts.unsqueeze(1) + torch.arange(plan.ratio, device=device)).flatten() 

259 return rows.long(), starts.long() 

260 

261 

262@lru_cache(maxsize=8) 

263@torch.inference_mode(False) 

264def _bank_slots(counts: tuple[int, ...], device: torch.device) -> torch.Tensor | None: 

265 """Materialize global padding maps only for the all-gather reference path.""" 

266 if len(set(counts)) == 1: 

267 return None 

268 capacity = max(counts) 

269 return torch.cat([torch.arange(peer*capacity, peer*capacity+count, device=device) 

270 for peer, count in enumerate(counts)])