Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / components / functional / lightning_indexer.py: 72%
89 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-25 05:45 +0800
« 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"""LightningIndexer V2 adapter for document-local causal TopK selection."""
17from __future__ import annotations
19from functools import lru_cache
20from typing import Any, Callable
22import torch # pylint: disable=forbidden-backend-import
24from hyper_parallel.components.functional.sparse_flash_mla import csa_tnd_plan
27# Float32 represents every integer up to this inclusive bound exactly.
28_FP32_EXACT_INT_LIMIT = 1 << 24
31def validate_indexer_tensors(query: torch.Tensor, key: torch.Tensor, weights: torch.Tensor) -> None:
32 """Check logical tensor relationships before packing Indexer or KL inputs.
34 Native operators validate their hardware-specific dtype and shape limits.
36 Args:
37 query: Indexer query [batch, queries, heads, dimension].
38 key: Indexer key bank [batch, keys, dimension].
39 weights: Per-query head weights [batch, queries, heads].
41 Raises:
42 ValueError: Tensor layouts, devices or query/key dtypes disagree.
43 """
44 if query.ndim != 4:
45 raise ValueError(f"Indexer query must have shape [B, Q, H, D], got {tuple(query.shape)}")
46 if key.ndim != 3 or key.shape[::2] != (query.shape[0], query.shape[-1]):
47 raise ValueError(f"Indexer key must have shape [B, K, D] matching Q, got Q={tuple(query.shape)}, "
48 f"K={tuple(key.shape)}")
49 if weights.shape != query.shape[:3]:
50 raise ValueError(f"Indexer weights must have shape {tuple(query.shape[:3])}, got {tuple(weights.shape)}")
51 if key.device != query.device or weights.device != query.device:
52 raise ValueError(f"Indexer Q/K/weights must share a device, got {query.device}, {key.device}, {weights.device}")
53 if key.dtype != query.dtype:
54 raise ValueError(f"Indexer Q/K must share a dtype, got {query.dtype} and {key.dtype}")
57def sort_key_indices(indices: torch.Tensor, maximum: int) -> torch.Tensor:
58 """Sort key IDs without losing integer precision or changing their dtype.
60 Args:
61 indices: Integer key IDs in [-1, maximum], including any sentinel.
62 maximum: A shape-derived upper bound; does not synchronize the device.
63 """
64 if maximum <= _FP32_EXACT_INT_LIMIT:
65 return indices.float().sort(dim=-1).values.to(indices.dtype)
66 return indices.sort(dim=-1).values
69@lru_cache(maxsize=1)
70def _load_indexer_op() -> Callable[..., Any]:
71 """Import the LI V2 entry only when fused selection is requested."""
72 # Keep the optional NPU extension out of CPU and eager-only imports.
73 try:
74 from cann_ops_transformer import ( # pylint: disable=import-outside-toplevel
75 lightning_indexer as lightning_indexer_op,
76 )
77 except ImportError as err:
78 raise RuntimeError(
79 "use_fused_ops=True requires cann_ops_transformer.lightning_indexer (LI V2). "
80 "See hyper_parallel/components/functional/README.md for ops-transformer installation, "
81 "environment setup and tested versions."
82 ) from err
83 return lightning_indexer_op
86@lru_cache(maxsize=1)
87def _load_indexer_metadata_op() -> Callable[..., Any]:
88 """Import the optional LI scheduling entry only when it will be used."""
89 # The verified 910B kernel computes its own split and does not read metadata.
90 try:
91 from cann_ops_transformer import lightning_indexer_metadata # pylint: disable=import-outside-toplevel
92 except ImportError as err:
93 raise RuntimeError(
94 "use_fused_ops=True requires cann_ops_transformer.lightning_indexer_metadata on this device. "
95 "See hyper_parallel/components/functional/README.md for ops-transformer installation, "
96 "environment setup and tested versions."
97 ) from err
98 return lightning_indexer_metadata
101@lru_cache(maxsize=16)
102def _uses_indexer_metadata(device: torch.device) -> bool:
103 """Omit unused LI scheduling metadata on the verified 910B path.
105 LI V2's arch22 kernel computes its split internally; arch35 requires
106 metadata. Keep the standard metadata call on other devices rather than
107 assuming they share the verified 910B behavior. TND still needs cu_seqlens.
108 """
109 return "910B" not in torch.npu.get_device_name(device)
112@lru_cache(maxsize=128)
113@torch.inference_mode(False)
114def _bsnd_indexer_metadata(device, batch, length, heads, dim, key_length, topk, ratio, residual):
115 """Reuse shape-only BSND metadata without caching inference-mode tensors."""
116 residue = torch.full((batch,), residual, device=device, dtype=torch.int32)
117 metadata = None
118 if _uses_indexer_metadata(device):
119 metadata = _load_indexer_metadata_op()(
120 heads, 1, dim, topk, cmp_residual_k=residue,
121 batch_size=batch, max_seqlen_q=length, max_seqlen_k=key_length,
122 layout_q="BSND", layout_k="BSND", mask_mode=3, cmp_ratio=ratio,
123 )
124 return residue, metadata
127@torch.no_grad()
128def _bsnd_lightning_indexer(
129 query: torch.Tensor, key: torch.Tensor, weights: torch.Tensor, ratio: int, topk: int,
130 segment: tuple[int, int, int, int, int],
131) -> torch.Tensor:
132 """Select one document segment per batch item in one BSND call.
134 Query tiling is owned by the operator, independently of the external
135 teacher chunk size. Multiple packed documents use the TND entry.
137 Args:
138 query: Local Indexer queries [batch, queries, heads, 128].
139 key: Global Indexer key bank [batch, compressed keys, 128].
140 weights: Local per-head merge weights, including model scaling.
141 ratio: Model compression ratio.
142 topk: Output selection width, including invalid tail slots.
143 segment: Local Q start/end, global compressed K start/end, and raw
144 prefix length modulo ratio, shared by all physical batch items.
145 """
146 batch, length, heads, dim = query.shape
147 result = torch.full((batch, length, topk), -1, device=query.device, dtype=torch.int32)
148 start, end, key_start, key_end, residual = segment
149 if key_end <= key_start or start == end:
150 return result
151 indexer = _load_indexer_op()
152 residue, metadata = _bsnd_indexer_metadata(
153 query.device, batch, end-start, heads, dim, key_end-key_start, topk, ratio, residual,
154 )
155 with torch.autocast(device_type=query.device.type, enabled=False):
156 indices, _ = indexer(
157 query[:, start:end].contiguous(), key[:, key_start:key_end].unsqueeze(2).contiguous(),
158 weights[:, start:end].float().contiguous(), topk,
159 cmp_residual_k=residue, metadata=metadata,
160 layout_q="BSND", layout_k="BSND", mask_mode=3, cmp_ratio=ratio,
161 )
162 indices = indices[:, :, 0]
163 indices = torch.where(indices >= 0, indices + key_start, key.shape[1])
164 indices = sort_key_indices(indices, key.shape[1])
165 result[:, start:end] = indices.masked_fill(indices == key.shape[1], -1)
166 return result
169@lru_cache(maxsize=128)
170@torch.inference_mode(False)
171def _tnd_indexer_metadata(plan, device, heads, dim, topk, ratio):
172 """Cache native selection metadata for the complete packed document partition."""
173 options = plan.indexer_options(device, ratio)
174 metadata = None
175 if _uses_indexer_metadata(device):
176 metadata = _load_indexer_metadata_op()(
177 heads, 1, dim, topk, batch_size=len(plan.segments),
178 max_seqlen_q=max(plan.q_lengths), max_seqlen_k=max(plan.k_lengths), **options,
179 )
180 return options, metadata
183@torch.no_grad()
184def lightning_indexer(
185 query: torch.Tensor, key: torch.Tensor, weights: torch.Tensor, ratio: int, topk: int, segments: tuple,
186) -> torch.Tensor:
187 """Select packed document prefixes in one TND call; single documents use BSND.
189 Args:
190 query: Local Indexer queries [batch, queries, heads, 128].
191 key: Global Indexer key bank [batch, compressed keys, 128].
192 weights: Per-query head weights including model scaling.
193 ratio: Model compression ratio.
194 topk: Selection width, including invalid tail slots.
195 segments: Local Q start/end, global document K start/prefix end and
196 raw-prefix residual. Packed documents require batch size one.
198 Returns:
199 Sorted global key IDs [batch, queries, topk], with invalid slots -1.
200 """
201 if len(segments) == 1:
202 return _bsnd_lightning_indexer(query, key, weights, ratio, topk, segments[0])
203 if query.shape[0] != 1:
204 raise ValueError("Packed Lightning Indexer requires batch size one")
205 result = torch.full((*query.shape[:2], topk), -1, device=query.device, dtype=torch.int32)
206 plan = csa_tnd_plan(segments, ratio)
207 if plan is None:
208 return result
209 options, metadata = _tnd_indexer_metadata(plan, query.device, query.shape[2], query.shape[3], topk, ratio)
210 with torch.autocast(device_type=query.device.type, enabled=False):
211 indices, _ = _load_indexer_op()(
212 plan.query(query), plan.key(key).unsqueeze(1), plan.query(weights).float(), topk,
213 metadata=metadata, **options,
214 )
215 offsets = plan.lengths(query.device)[3]
216 indices = torch.where(indices[:, 0] >= 0, indices[:, 0] + offsets, key.shape[1])
217 indices = sort_key_indices(indices, key.shape[1])
218 indices = indices.masked_fill(indices == key.shape[1], -1)
219 plan.restore(indices, result)
220 return result