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

213 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"""Packed token metadata and causal convolution shared by linear attention.""" 

16 

17from __future__ import annotations 

18 

19from bisect import bisect_left, bisect_right 

20from dataclasses import dataclass, field 

21from typing import Any, Callable, Mapping, Sequence 

22 

23import torch 

24from torch import nn 

25from torch.nn import functional as F 

26 

27from hyper_parallel.components.functional.linear_attention_convolution import ( 

28 fused_causal_conv1d, validate_conv_backend, 

29) 

30 

31 

32# Bound retained interval views and their materialized chunk indices. Evicted 

33# views can be rebuilt; this cache budget does not limit CP degree or documents. 

34_MAX_CACHED_INTERVALS = 32 

35 

36 

37@dataclass(frozen=True) 

38class PackedChunkPlan: 

39 """Read-only kernel indices for one local packed interval and chunk size.""" 

40 

41 cu_seqlens: torch.Tensor 

42 cu_seqlens_cpu: torch.Tensor 

43 chunk_indices: torch.Tensor 

44 chunk_offsets: torch.Tensor 

45 

46 def kwargs(self) -> dict[str, torch.Tensor]: 

47 """Return the common FLA varlen arguments without copying device tensors.""" 

48 return {"cu_seqlens": self.cu_seqlens, "chunk_indices": self.chunk_indices} 

49 

50 

51@dataclass(frozen=True, eq=False) 

52class PackedDocumentInfo: 

53 """Validated immutable document snapshot shared by a batch's interval views. 

54 

55 Identity hashing lets bounded CPU-plan caches reuse this snapshot without 

56 hashing every document offset at every layer. Cache keys retain the object, 

57 so distinct batches cannot collide through reuse of a Python object ID. 

58 """ 

59 

60 offsets: tuple[int, ...] 

61 

62 def __post_init__(self) -> None: 

63 offsets = tuple(self.offsets) 

64 if ( 

65 len(offsets) < 2 

66 or offsets[0] != 0 

67 or any(not isinstance(value, int) or isinstance(value, bool) for value in offsets) 

68 or any(left >= right for left, right in zip(offsets, offsets[1:])) 

69 ): 

70 raise ValueError("Packed sample offsets must be strictly increasing integers starting at zero.") 

71 object.__setattr__(self, "offsets", offsets) 

72 

73 

74@dataclass(frozen=True) 

75class PackedSequenceInfo: 

76 """Own immutable sample offsets and a contiguous physical token interval. 

77 

78 Offsets are global within one CP group, including any explicitly declared 

79 padding sample. Local CP cuts are fragments, never new sample resets. 

80 Device plans contain read-only tensors; each invocation saves the tensors 

81 it consumes so that ordinary autograd version checks remain effective. 

82 """ 

83 

84 global_offsets: tuple[int, ...] | PackedDocumentInfo 

85 start: int 

86 end: int 

87 local_offsets: tuple[int, ...] = field(init=False) 

88 documents: PackedDocumentInfo = field(init=False, repr=False, compare=False, hash=False) 

89 _head_index: int = field(init=False, repr=False, compare=False, hash=False) 

90 _tail_index: int = field(init=False, repr=False, compare=False, hash=False) 

91 _plans: dict[tuple[torch.device, int], PackedChunkPlan] = field( 

92 default_factory=dict, init=False, repr=False, compare=False, hash=False 

93 ) 

94 _intervals: dict[tuple[int, int], PackedSequenceInfo] = field( 

95 default_factory=dict, init=False, repr=False, compare=False, hash=False 

96 ) 

97 

98 def __post_init__(self) -> None: 

99 documents = (self.global_offsets if isinstance(self.global_offsets, PackedDocumentInfo) 

100 else PackedDocumentInfo(self.global_offsets)) 

101 offsets = documents.offsets 

102 if ( 

103 not isinstance(self.start, int) or isinstance(self.start, bool) 

104 or not isinstance(self.end, int) or isinstance(self.end, bool) 

105 or not 0 <= self.start < self.end <= offsets[-1] 

106 ): 

107 raise ValueError("Packed token interval must be nonempty and covered by global sample offsets.") 

108 head = bisect_right(offsets, self.start) - 1 

109 tail = bisect_left(offsets, self.end) - 1 

110 local = (0, *(value - self.start for value in offsets[head + 1:tail + 1]), self.end - self.start) 

111 object.__setattr__(self, "global_offsets", offsets) 

112 object.__setattr__(self, "local_offsets", local) 

113 object.__setattr__(self, "documents", documents) 

114 object.__setattr__(self, "_head_index", head) 

115 object.__setattr__(self, "_tail_index", tail) 

116 

117 @property 

118 def length(self) -> int: 

119 """Return this interval's physical token count.""" 

120 return self.end - self.start 

121 

122 @property 

123 def num_sequences(self) -> int: 

124 """Return the number of nonempty local sample fragments.""" 

125 return len(self.local_offsets) - 1 

126 

127 @property 

128 def continues_previous(self) -> bool: 

129 """Whether the first fragment consumes the preceding interval's state.""" 

130 return self.start != self.global_offsets[self._head_index] 

131 

132 @property 

133 def continues_next(self) -> bool: 

134 """Whether the last fragment feeds the following interval's state.""" 

135 return self.end != self.global_offsets[self._tail_index + 1] 

136 

137 @property 

138 def transfer_active(self) -> bool: 

139 """Whether the preceding interval can affect this interval's exit.""" 

140 return self.continues_previous and self.num_sequences == 1 

141 

142 @property 

143 def head(self) -> slice: 

144 """Return the first fragment's local token slice.""" 

145 return slice(0, self.local_offsets[1]) 

146 

147 @property 

148 def tail(self) -> slice: 

149 """Return the last fragment's local token slice.""" 

150 return slice(self.local_offsets[-2], self.length) 

151 

152 @property 

153 def preceding_sample_tokens(self) -> int: 

154 """Return how many earlier tokens belong to the first local sample.""" 

155 return self.start - self.global_offsets[self._head_index] 

156 

157 def interval(self, start: int, end: int) -> PackedSequenceInfo: 

158 """Describe another contiguous interval without retaining artificial cuts.""" 

159 key = (start, end) 

160 if key == (self.start, self.end): 

161 return self 

162 if key not in self._intervals: 

163 if len(self._intervals) == _MAX_CACHED_INTERVALS: 

164 self._intervals.pop(next(iter(self._intervals))) 

165 self._intervals[key] = PackedSequenceInfo(self.documents, start, end) 

166 return self._intervals[key] 

167 

168 def ulysses(self, degree: int, local_rank: int) -> PackedSequenceInfo: 

169 """Describe the token interval after consecutive-rank AllToAll.""" 

170 if degree < 1 or not 0 <= local_rank < degree: 

171 raise ValueError("Invalid Ulysses degree or local rank.") 

172 start = self.start - local_rank * self.length 

173 return self.interval(start, start + degree * self.length) 

174 

175 def materialize(self, device: torch.device, chunk_size: int) -> PackedChunkPlan: 

176 """Prepare immutable indices once per batch, layout, device and chunk size.""" 

177 device = torch.device(device) 

178 cache_key = (device, chunk_size) 

179 if cache_key in self._plans: 

180 return self._plans[cache_key] 

181 if chunk_size < 1: 

182 raise ValueError("chunk_size must be positive.") 

183 counts = [ 

184 (end - start + chunk_size - 1) // chunk_size 

185 for start, end in zip(self.local_offsets, self.local_offsets[1:]) 

186 ] 

187 indices = [(sequence, chunk) for sequence, count in enumerate(counts) for chunk in range(count)] 

188 offsets = [0] 

189 for count in counts: 

190 offsets.append(offsets[-1] + count) 

191 cpu = torch.tensor(self.local_offsets, dtype=torch.int64) 

192 plan = PackedChunkPlan( 

193 cpu.to(device), 

194 cpu, 

195 torch.tensor(indices, device=device, dtype=torch.int64), 

196 torch.tensor(offsets, device=device, dtype=torch.int64), 

197 ) 

198 self._plans[cache_key] = plan 

199 return plan 

200 

201 def first_state(self, state: torch.Tensor | None) -> torch.Tensor | None: 

202 """Expand only a continued first fragment's state for a varlen kernel.""" 

203 if state is None or not self.continues_previous: 

204 return None 

205 if self.num_sequences == 1: 

206 return state 

207 return torch.cat((state, state.new_zeros((self.num_sequences - 1, *state.shape[1:])))) 

208 

209 def last_state(self, state: torch.Tensor | None) -> torch.Tensor | None: 

210 """Expand a terminal state gradient into the last fragment slot.""" 

211 if state is None or not self.continues_next: 

212 return None 

213 if self.num_sequences == 1: 

214 return state 

215 return torch.cat((state.new_zeros((self.num_sequences - 1, *state.shape[1:])), state)) 

216 

217 

218def _offset_tuple(value: torch.Tensor | Sequence[int]) -> tuple[int, ...]: 

219 """Snapshot user metadata so a later batch cannot mutate saved semantics.""" 

220 if isinstance(value, torch.Tensor): 

221 if value.ndim != 1 or value.dtype not in (torch.int32, torch.int64): 

222 raise ValueError("Cumulative sample offsets must be a one-dimensional integer tensor.") 

223 return tuple(value.detach().cpu().tolist()) 

224 return tuple(value) 

225 

226 

227def _global_offsets(info: PackedSequenceInfo | None, aliases: list) -> tuple[int, ...]: 

228 """Check alias agreement before constructing or reusing local metadata.""" 

229 if info is not None and not isinstance(info, PackedSequenceInfo): 

230 raise TypeError("packed_sequence_info must be PackedSequenceInfo.") 

231 offsets = info.global_offsets if info is not None else _offset_tuple(aliases[0]) 

232 # Runtime q/k aliases share this immutable tuple; comparing the same tuple 

233 # still walks its elements in Python, so skip only exact object identity. 

234 if any(_offset_tuple(value) != offsets for value in aliases if value is not offsets): 

235 raise ValueError("Packed sequence aliases disagree; provide one consistent set of global sample offsets.") 

236 return offsets 

237 

238 

239def resolve_packed_sequence( 

240 arguments: Mapping[str, Any], 

241 local_length: int, 

242 *, 

243 cp_rank: int = 0, 

244 cp_size: int = 1, 

245) -> PackedSequenceInfo | None: 

246 """Normalize model-level aliases, all of which describe GLOBAL sample offsets. 

247 

248 Kernel-level cu_seqlens is derived from the returned local interval. Prefer 

249 a prebuilt packed_sequence_info from the runtime adapter to avoid host reads 

250 or per-layer validation of device metadata. 

251 """ 

252 info = arguments.get("packed_sequence_info") 

253 aliases = [ 

254 arguments[name] 

255 for name in ("cu_seq_lens", "cu_seqlens", "cu_seq_lens_q", "cu_seq_lens_k") 

256 if arguments.get(name) is not None 

257 ] 

258 if arguments.get("packed_seq_params") is not None or arguments.get("seq_idx") is not None: 

259 raise ValueError( 

260 "Use global cu_seq_lens or PackedSequenceInfo; implicit packed/seq_idx conversion is unsupported." 

261 ) 

262 if info is None and not aliases: 

263 return None 

264 if not 0 <= cp_rank < cp_size: 

265 raise ValueError("Invalid CP rank or size for packed sequence metadata.") 

266 offsets = _global_offsets(info, aliases) 

267 if info is None: 

268 info = PackedSequenceInfo(offsets, cp_rank * local_length, (cp_rank + 1) * local_length) 

269 if ( 

270 info.length != local_length 

271 or info.start != cp_rank * local_length 

272 or info.global_offsets[-1] != cp_size * local_length 

273 ): 

274 raise ValueError("Packed metadata must cover the chronological, equal-length CP token intervals.") 

275 return info 

276 

277 

278class _PackedConvTap(torch.autograd.Function): 

279 """Recreate a masked tap in backward instead of retaining its FP32 copy.""" 

280 

281 @staticmethod 

282 def forward( 

283 ctx: Any, extended: torch.Tensor, weight: torch.Tensor, valid: torch.Tensor, offset: int, length: int, 

284 ) -> torch.Tensor: 

285 """Save shared input storage while preserving the eager product precision.""" 

286 ctx.save_for_backward(extended, weight, valid) 

287 ctx.offset, ctx.length = offset, length 

288 values = extended[:, offset : offset + length].to(weight.dtype) 

289 values = torch.where(valid[None, :, None], values, 0.0) 

290 return values * weight[None, None, :] 

291 

292 @staticmethod 

293 def backward( 

294 ctx: Any, grad_output: torch.Tensor, 

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

296 """Rebuild only this tap; differentiable Torch operations retain higher derivatives.""" 

297 extended, weight, valid = ctx.saved_tensors 

298 grad_extended = grad_weight = None 

299 if ctx.needs_input_grad[0]: 

300 grad_values = grad_output * weight[None, None, :] 

301 grad_values = torch.where(valid[None, :, None], grad_values, 0.0).to(extended.dtype) 

302 grad_extended = F.pad(grad_values, (0, 0, ctx.offset, extended.shape[1] - ctx.offset - ctx.length)) 

303 if ctx.needs_input_grad[1]: 

304 values = extended[:, ctx.offset : ctx.offset + ctx.length].to(weight.dtype) 

305 values = torch.where(valid[None, :, None], values, 0.0) 

306 grad_weight = (grad_output * values).sum(dim=(0, 1)) 

307 return grad_extended, grad_weight, None, None, None 

308 

309 

310def packed_causal_conv1d( 

311 inputs: torch.Tensor, 

312 convolution: nn.Conv1d, 

313 info: PackedSequenceInfo, 

314 halo: torch.Tensor | None = None, 

315 *, 

316 weight: torch.Tensor | None = None, 

317 bias: torch.Tensor | None = None, 

318 backend: str = "eager", 

319) -> torch.Tensor: 

320 """Apply depthwise causal SiLU convolution with exact sample isolation. 

321 

322 Vectorize over tokens rather than loop over samples. Accumulate low-precision 

323 products in FP32; keep FP32/FP64 reference inputs at their original precision. 

324 Masking taps also masks their autograd contribution to an exchanged halo. 

325 Backward reconstructs each masked tap from the shared input rather than 

326 retaining a full accumulation-precision input for every window position. 

327 

328 Args: 

329 inputs: Local projected tokens with shape ``[1, T, C]``. 

330 convolution: Source depthwise, stride-one convolution attributes and parameters. 

331 info: True document boundaries and the current local token interval. 

332 halo: Optional already exchanged causal history; this function adds no communication. 

333 weight: Optional local parameter view with shape ``[C, 1, W]``. 

334 bias: Optional local bias; used with an explicit weight override. 

335 backend: ``eager`` by default, or explicitly ``triton`` / ``ascendc`` 

336 for optional NPU kernels. The selected backend never silently falls back. 

337 

338 Returns: 

339 SiLU output with the input shape and dtype. 

340 

341 Raises: 

342 ValueError: If metadata, depthwise shape, or the selected kernel contract is invalid. 

343 ImportError: If an explicitly selected optional kernel package is unavailable. 

344 """ 

345 validate_conv_backend(backend) 

346 if inputs.ndim != 3 or inputs.shape[:2] != (1, info.length): 

347 raise ValueError("Packed convolution expects [1,T,C] matching its token interval.") 

348 channels = inputs.shape[-1] 

349 if ( 

350 convolution.groups != convolution.in_channels 

351 or convolution.out_channels != convolution.in_channels 

352 or convolution.stride != (1,) 

353 ): 

354 raise ValueError("Packed convolution requires a stride-one depthwise Conv1d.") 

355 if weight is None: 

356 weight, bias = convolution.weight, convolution.bias 

357 width, dilation = convolution.kernel_size[0], convolution.dilation[0] 

358 if weight.shape != (channels, 1, width): 

359 raise ValueError("Packed convolution weight must match the local head channels.") 

360 halo_width = (width - 1) * dilation 

361 if halo is not None and halo.shape != (1, halo_width, channels): 

362 raise ValueError("Packed convolution halo has an incompatible shape.") 

363 if backend != "eager": 

364 return fused_causal_conv1d( 

365 inputs, weight, bias, info, halo, backend=backend, dilation=dilation, 

366 ) 

367 if halo is None: 

368 halo = inputs.new_zeros((1, halo_width, channels)) 

369 extended = torch.cat((halo, inputs), dim=1) 

370 positions = torch.arange(info.start, info.end, device=inputs.device) 

371 boundaries = torch.tensor(info.global_offsets, device=inputs.device) 

372 sample_indices = torch.bucketize(positions, boundaries[1:], right=True) 

373 starts = boundaries[sample_indices] 

374 dtype = torch.float32 if inputs.dtype in (torch.bfloat16, torch.float16) else inputs.dtype 

375 output = torch.zeros_like(inputs, dtype=dtype) 

376 weights = weight[:, 0].to(dtype) 

377 for tap in range(width): 

378 valid = positions - (width - 1 - tap) * dilation >= starts 

379 output = output + _PackedConvTap.apply(extended, weights[:, tap], valid, tap * dilation, info.length) 

380 if bias is not None: 

381 output = output + bias.to(dtype) 

382 return F.silu(output.to(inputs.dtype)) 

383 

384 

385def packed_reference( 

386 operator: Callable[..., tuple[torch.Tensor, torch.Tensor | None]], 

387 inputs: tuple[torch.Tensor, ...], 

388 cu_seqlens: torch.Tensor | Sequence[int], 

389 *, 

390 initial_state: torch.Tensor | None = None, 

391 output_final_state: bool = False, 

392 **kwargs: Any, 

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

394 """Evaluate the portable reference independently per sample fragment.""" 

395 if inputs[0].shape[0] != 1: 

396 raise ValueError("Packed reference requires batch dimension one.") 

397 info = PackedSequenceInfo(_offset_tuple(cu_seqlens), 0, inputs[0].shape[1]) 

398 if info.global_offsets[-1] != info.length: 

399 raise ValueError("Local cumulative offsets must end at the input token count.") 

400 if initial_state is not None and initial_state.shape[0] != info.num_sequences: 

401 raise ValueError("Packed initial_state requires one slot per local sample fragment.") 

402 outputs, finals = [], [] 

403 for index, (start, end) in enumerate(zip(info.local_offsets, info.local_offsets[1:])): 

404 state = None if initial_state is None else initial_state[index : index + 1] 

405 output, final = operator( 

406 *(tensor[:, start:end] for tensor in inputs), 

407 initial_state=state, 

408 output_final_state=output_final_state, 

409 **kwargs, 

410 ) 

411 outputs.append(output) 

412 if output_final_state: 

413 finals.append(final) 

414 return torch.cat(outputs, dim=1), torch.cat(finals) if output_final_state else None