Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / components / functional / lightning_indexer.py: 85%
93 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +0800
« 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"""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 # Rebuild singleton strides; contiguous() alone may retain the full-bank batch stride.
156 native_key = key[:, key_start:key_end].contiguous().view(batch, key_end-key_start, 1, dim)
157 with torch.autocast(device_type=query.device.type, enabled=False):
158 # Singleton batch strides may survive contiguous(); view canonicalizes the native BSND layout.
159 native_key = key[:, key_start:key_end].contiguous().view(batch, key_end-key_start, 1, dim)
160 indices, _ = indexer(
161 query[:, start:end].contiguous(), native_key,
162 weights[:, start:end].float().contiguous(), topk,
163 cmp_residual_k=residue, metadata=metadata,
164 layout_q="BSND", layout_k="BSND", mask_mode=3, cmp_ratio=ratio,
165 )
166 indices = indices[:, :, 0]
167 indices = torch.where(indices >= 0, indices + key_start, key.shape[1])
168 indices = sort_key_indices(indices, key.shape[1])
169 result[:, start:end] = indices.masked_fill(indices == key.shape[1], -1)
170 return result
173@lru_cache(maxsize=128)
174@torch.inference_mode(False)
175def _tnd_indexer_metadata(plan, device, heads, dim, topk, ratio):
176 """Cache native selection metadata for the complete packed document partition."""
177 options = plan.indexer_options(device, ratio)
178 metadata = None
179 if _uses_indexer_metadata(device):
180 metadata = _load_indexer_metadata_op()(
181 heads, 1, dim, topk, batch_size=len(plan.segments),
182 max_seqlen_q=max(plan.q_lengths), max_seqlen_k=max(plan.k_lengths), **options,
183 )
184 return options, metadata
187@torch.no_grad()
188def lightning_indexer(
189 query: torch.Tensor, key: torch.Tensor, weights: torch.Tensor, ratio: int, topk: int, segments: tuple,
190) -> torch.Tensor:
191 """Select packed document prefixes in one TND call; single documents use BSND.
193 Args:
194 query: Local Indexer queries [batch, queries, heads, 128].
195 key: Global Indexer key bank [batch, compressed keys, 128].
196 weights: Per-query head weights including model scaling.
197 ratio: Model compression ratio.
198 topk: Selection width, including invalid tail slots.
199 segments: Local Q start/end, global document K start/prefix end and
200 raw-prefix residual. Packed documents require batch size one.
202 Returns:
203 Sorted global key IDs [batch, queries, topk], with invalid slots -1.
204 """
205 if len(segments) == 1:
206 return _bsnd_lightning_indexer(query, key, weights, ratio, topk, segments[0])
207 if query.shape[0] != 1:
208 raise ValueError("Packed Lightning Indexer requires batch size one")
209 plan = csa_tnd_plan(segments, ratio)
210 if plan is None:
211 return torch.full((*query.shape[:2], topk), -1, device=query.device, dtype=torch.int32)
212 options, metadata = _tnd_indexer_metadata(plan, query.device, query.shape[2], query.shape[3], topk, ratio)
213 with torch.autocast(device_type=query.device.type, enabled=False):
214 indices, _ = _load_indexer_op()(
215 plan.query(query), plan.key(key).unsqueeze(1), plan.query(weights).float(), topk,
216 metadata=metadata, **options,
217 )
218 offsets = plan.lengths(query.device)[3]
219 indices = torch.where(indices[:, 0] >= 0, indices[:, 0] + offsets, key.shape[1])
220 indices = sort_key_indices(indices, key.shape[1])
221 indices = indices.masked_fill(indices == key.shape[1], -1)
222 if plan.covers_queries(query.shape[1]):
223 return indices.unsqueeze(0)
224 result = torch.full((*query.shape[:2], topk), -1, device=query.device, dtype=torch.int32)
225 plan.restore(indices, result)
226 return result