File size: 15,539 Bytes
4d9b003 be62f78 4d9b003 be62f78 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 | # SPDX-License-Identifier: Apache-2.0
"""C21: top-k query selection on the device (PLAN.md section 1.2 row C21; probe P8, common/PROBES.md).
The query-based LiDAR detectors (TransFusion, BEVFusion, PTv3) pick the ``K`` best (class, cell) proposals of a
local-max heatmap ``[C, H*W]``. On the p150 that is ``ttnn.experimental.topk_large_indices`` (Blackhole only): one
bf16 ROW_MAJOR row, ``k`` a multiple of 16 in [16, 2048], uint32 indices in descending value order, ~4.3 ns per
element on one core (0.71 / 0.80 / 1.98 ms over 162k / 184k / 459k values, P8). What the helpers here guarantee:
- **The row is class-major** (:func:`class_major_row`): index ``c * cells + p`` for class ``c`` and cell ``p`` --
the ONNX ``TopK`` order of a ``[1, C, H*W]`` reshape, so reference (teacher-forcing) indices need no conversion.
The NHWC heatmap ``[1, 1, cells, C]`` (TILE) is transposed (one program), reshaped to one row while still tiled,
and untilized: no ROW_MAJOR reshape of wide pages, whose L1 staging (2 x the destination page, API.md 13) would
not fit PT's 0.9 MB row (TF's is 0.37 MB).
- **Exact decode** (:func:`decode_class_major`): the uint32 indices are cast to fp32 (exact below 2**24) and decoded
with comparisons only, ``cls = sum_c (idx >= c * cells)``, ``pos = idx - cls * cells`` -- no division, so no
device rounding can move a boundary -- clamped into range (a 0xFFFFFFFF sentinel can never become an
out-of-range gather index) and cast back to uint32 ROW_MAJOR ``[1, K]`` rows (``ttnn.embedding`` indices).
- **Ties are unspecified** (P8: the device does not break ties by lowest index), so every gate after the selection
is set-based (:func:`selection_metrics`) or teacher-forced; the host oracle :func:`topk_numpy` has ONNX semantics
(stable descending: ties to the lower index).
- **Never ``-inf`` masks**: surviving ``-inf`` values come back as index 0xFFFFFFFF (P8); mask with
:data:`MASK_VALUE` (-1e30, finite in bf16) instead.
- ``k`` is padded to a multiple of 16 (:func:`padded_k`; 500 -> 512); keep the first ``K`` of the descending output.
The bf16 ``ttnn.topk`` composite also returns the values (P8: +8-98 us, exact values): :func:`topk_with_values`. An
fp32 value re-gather costs ~1 us per index through ``ttnn.gather`` (P13); a two-stage top-k (row-split first stage)
is optimization work (PLAN.md 5.3).
numpy only at import; ttnn is imported inside the device functions.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Sequence, Tuple
import numpy as np
__all__ = ["K_MULTIPLE", "K_MIN", "K_MAX", "MASK_VALUE", "SENTINEL", "padded_k", "class_major_row", "topk_indices",
"decode_class_major", "topk_with_values", "TopKSelect", "topk_numpy", "class_major_numpy",
"decode_class_major_numpy", "selection_metrics", "near_tie_report", "local_max_margins"]
K_MULTIPLE = 16
K_MIN = 16
K_MAX = 2048
MASK_VALUE = -1.0e30 # masking value for scores (never -inf: P8 sentinel)
SENTINEL = 0xFFFFFFFF # index returned for a surviving -inf
FP32_EXACT = 1 << 24 # integers below are exact in fp32 (the decode's range)
def padded_k(k: int) -> int:
"""``k`` rounded up to the multiple of 16 ``topk_large_indices`` takes (500 -> 512); 16 <= result <= 2048."""
k = int(k)
if k <= 0:
raise ValueError(f"k must be positive, got {k}")
kp = max(K_MIN, -(-k // K_MULTIPLE) * K_MULTIPLE)
if kp > K_MAX:
raise ValueError(f"k={k} exceeds topk_large_indices' maximum {K_MAX}")
return kp
# ------------------------------------------------------------------------------------------------- device
def class_major_row(heat_nhwc: Any, classes: int, cells: int, *, memory_config: Any = None):
"""NHWC heatmap ``[1, 1, cells, classes]`` (TILE, bf16) -> the class-major bf16 ROW_MAJOR row
``[1, classes * cells]`` of the top-k (transpose, TILE reshape, untilize: traceable)."""
import ttnn
shape = tuple(int(s) for s in heat_nhwc.shape)
if shape != (1, 1, int(cells), int(classes)):
raise ValueError(f"heat must be [1, 1, {cells}, {classes}], got {shape}")
if heat_nhwc.dtype != ttnn.bfloat16:
raise ValueError(f"topk_large_indices takes bf16 scores, got {heat_nhwc.dtype}")
kw = {} if memory_config is None else {"memory_config": memory_config}
n = int(classes) * int(cells)
t = ttnn.transpose(heat_nhwc, 2, 3, **kw) # [1, 1, classes, cells] TILE
flat = ttnn.reshape(t, (1, 1, 1, n)) # the row while still tiled (TILE reshape)
ttnn.deallocate(t, False)
rm = ttnn.to_layout(flat, ttnn.ROW_MAJOR_LAYOUT) # one row, untilized
ttnn.deallocate(flat, False)
return ttnn.reshape(rm, (1, n))
def topk_indices(row: Any, k: int):
"""``topk_large_indices`` of a bf16 ROW_MAJOR ``[rows, n]`` tensor -> uint32 ROW_MAJOR ``[rows, padded_k(k)]``,
descending (ties unspecified). Refuses an ``n`` beyond fp32's exact integers (the decode's range)."""
import ttnn
n = int(row.shape[-1])
kp = padded_k(k)
if n < kp:
raise ValueError(f"the row has {n} values, fewer than k={kp}")
if n > FP32_EXACT:
raise ValueError(f"row of {n} values: indices above 2**24 are not exact in the fp32 decode")
if row.dtype != ttnn.bfloat16 or row.layout != ttnn.ROW_MAJOR_LAYOUT:
raise ValueError("topk_large_indices takes a bf16 ROW_MAJOR row")
return ttnn.experimental.topk_large_indices(row, k=kp)
def decode_class_major(indices: Any, cells: int, classes: int, *, clamp: bool = True) -> Tuple[Any, Any]:
"""Class-major flat indices (uint32, ROW_MAJOR ``[1, K]`` or TILE) -> ``(pos, cls)`` uint32 ROW_MAJOR ``[1, K]``
with ``idx = cls * cells + pos`` (module docstring: fp32 comparisons, exact; ``clamp`` keeps them in range)."""
import ttnn
cells, classes = int(cells), int(classes)
if cells * classes > FP32_EXACT:
raise ValueError("cells * classes exceeds fp32's exact integers")
it = indices if indices.layout == ttnn.TILE_LAYOUT else ttnn.to_layout(indices, ttnn.TILE_LAYOUT)
f = ttnn.typecast(it, ttnn.float32)
if it is not indices:
ttnn.deallocate(it, False)
cls = None
for c in range(1, classes):
ge = ttnn.ge(f, float(c * cells))
if cls is None:
cls = ge
else:
s = ttnn.add(cls, ge)
ttnn.deallocate(cls, False)
ttnn.deallocate(ge, False)
cls = s
if cls is None: # one class: cls = 0, pos = idx
pos = f
cls = ttnn.multiply(f, 0.0)
else:
off = ttnn.multiply(cls, float(cells))
pos = ttnn.subtract(f, off)
ttnn.deallocate(off, False)
ttnn.deallocate(f, False)
if clamp:
p2 = ttnn.clamp(pos, 0.0, float(cells - 1))
c2 = ttnn.clamp(cls, 0.0, float(classes - 1))
ttnn.deallocate(pos, False)
ttnn.deallocate(cls, False)
pos, cls = p2, c2
out = []
for t in (pos, cls):
u = ttnn.typecast(t, ttnn.uint32)
ttnn.deallocate(t, False)
r = ttnn.to_layout(u, ttnn.ROW_MAJOR_LAYOUT)
ttnn.deallocate(u, False)
out.append(r)
return out[0], out[1]
def topk_with_values(scores_tile: Any, k: int):
"""The bf16 ``ttnn.topk`` composite on a TILE ``[.., n]`` row: ``(values bf16, indices)`` of the top ``k``
(P8: exact values, in-range indices even with ``-inf``; ties unspecified)."""
import ttnn
return ttnn.topk(scores_tile, k=int(k), dim=-1, largest=True, sorted=True)
class TopKSelect:
"""Top-``k`` of a ``[1, 1, cells, classes]`` bf16 TILE heatmap -> uint32 class-major indices ``[1, padded_k]``
(descending) plus the decoded ``(pos, cls)`` rows: :func:`class_major_row`, :func:`topk_indices`,
:func:`decode_class_major`. ``keep`` (default ``k``) proposals are meaningful; the rows hold ``padded_k(k)``."""
def __init__(self, cells: int, classes: int, k: int, *, name: str = "topk"):
self.cells, self.classes, self.k = int(cells), int(classes), int(k)
self.kp = padded_k(self.k)
if self.cells * self.classes < self.kp:
raise ValueError(f"{name}: {self.cells * self.classes} scores for k={self.kp}")
self.name = name
def indices(self, heat_nhwc: Any):
row = class_major_row(heat_nhwc, self.classes, self.cells)
idx = topk_indices(row, self.kp)
import ttnn
ttnn.deallocate(row, False)
return idx
def decode(self, indices: Any) -> Tuple[Any, Any]:
return decode_class_major(indices, self.cells, self.classes)
def __call__(self, heat_nhwc: Any) -> Tuple[Any, Any, Any]:
"""-> ``(indices, pos, cls)``, each uint32 ROW_MAJOR ``[1, padded_k]``."""
idx = self.indices(heat_nhwc)
pos, cls = self.decode(idx)
return idx, pos, cls
def describe(self) -> Dict[str, Any]:
return {"name": self.name, "cells": self.cells, "classes": self.classes, "k": self.k, "padded_k": self.kp,
"order": "class-major (idx = cls * cells + pos)", "ties": "unspecified (P8)"}
# --------------------------------------------------------------------------------------------------- host
def topk_numpy(scores: Any, k: int) -> Tuple[np.ndarray, np.ndarray]:
"""ONNX ``TopK(largest=1, sorted=1)`` of a 1-D score vector: ``(values, indices)``, values descending, ties in
ascending index order (a stable descending sort)."""
s = np.asarray(scores).reshape(-1)
order = np.argsort(-s.astype(np.float64), kind="stable")[: int(k)]
return s[order], order.astype(np.int64)
def class_major_numpy(heat_nhwc: Any) -> np.ndarray:
"""NHWC rows ``[cells, classes]`` -> the class-major flat vector of :func:`class_major_row`."""
h = np.asarray(heat_nhwc)
return np.ascontiguousarray(h.reshape(-1, h.shape[-1]).T).reshape(-1)
def decode_class_major_numpy(indices: Any, cells: int) -> Tuple[np.ndarray, np.ndarray]:
"""Host decode: ``(pos, cls)`` int64 of class-major flat indices."""
i = np.asarray(indices).astype(np.int64).reshape(-1)
return i % int(cells), i // int(cells)
def selection_metrics(device_indices: Any, reference_indices: Any, *, reference_scores: Optional[Any] = None,
flat_scores: Optional[Any] = None, strong: float = 0.1, k: Optional[int] = None
) -> Dict[str, Any]:
"""Set-based agreement of a device top-k with the reference's (PLAN.md 2.5 / 0.3 item 1).
``device_indices`` / ``reference_indices``: flat indices (the first ``k`` of each are compared, default the
reference length). ``reference_scores``: the reference values of ``reference_indices`` (same order) or
``flat_scores``: the reference score of every flat index. Returns ``shared`` (count) / ``shared_frac``, and with
scores the strong proposals (reference score >= ``strong``): ``strong_total``, ``strong_kept``,
``strong_overlap`` (fraction; 1.0 when there is none), ``strong_missed`` (indices)."""
d = np.asarray(device_indices).astype(np.int64).reshape(-1)
r = np.asarray(reference_indices).astype(np.int64).reshape(-1)
k = len(r) if k is None else int(k)
d, r = d[:k], r[:k]
ds = set(d.tolist())
out: Dict[str, Any] = {"k": k, "shared": len(ds & set(r.tolist())), "unique": len(ds) == len(d)}
out["device_only"] = sorted(ds - set(r.tolist()))
out["shared_frac"] = out["shared"] / max(k, 1)
if reference_scores is not None or flat_scores is not None:
if reference_scores is not None:
vals = np.asarray(reference_scores, np.float64).reshape(-1)[:k]
strong_set = set(r[vals >= strong].tolist())
else:
fs = np.asarray(flat_scores, np.float64).reshape(-1)
strong_set = set(np.flatnonzero(fs >= strong).tolist())
missed = sorted(strong_set - ds)
out.update({"strong_threshold": float(strong), "strong_total": len(strong_set),
"strong_kept": len(strong_set) - len(missed),
"strong_overlap": 1.0 if not strong_set else (len(strong_set) - len(missed)) / len(strong_set),
"strong_missed": missed})
return out
def near_tie_report(missed: Sequence[int], device_indices: Any, reference_heat: Any, height: int, width: int,
*, kernel: int = 3) -> List[Dict[str, Any]]:
"""Why a reference proposal is missing from a device selection (diagnostics of :func:`selection_metrics`).
``reference_heat``: the reference's class-major sigmoid heat BEFORE the local max (``[C * H * W]`` or
``[C, H, W]``). For each missed class-major index: its heat, the strongest same-class cell of its ``kernel x
kernel`` window that the DEVICE selected, and the relative gap ``(heat_missed - heat_neighbour) / heat_missed``
(a few 0.1 % = a local-max near-tie: the neighbour won the device's 3x3 comparison; a bf16 heat holds 3
significant digits). ``neighbour`` is None when no same-class neighbour was selected."""
heat = np.asarray(reference_heat, np.float64).reshape(-1)
cells = int(height) * int(width)
sel = set(np.asarray(device_indices).astype(np.int64).reshape(-1).tolist())
r = int(kernel) // 2
out: List[Dict[str, Any]] = []
for idx in missed:
c, p = divmod(int(idx), cells)
y, x = divmod(p, int(width))
best = None
for dy in range(-r, r + 1):
for dx in range(-r, r + 1):
yy, xx = y + dy, x + dx
if (dy or dx) and 0 <= yy < height and 0 <= xx < width:
q = c * cells + yy * int(width) + xx
if q in sel and (best is None or heat[q] > heat[best]):
best = q
rec: Dict[str, Any] = {"index": int(idx), "class": c, "cell": [y, x], "heat": float(heat[idx]),
"neighbour": None if best is None else int(best)}
if best is not None:
rec["neighbour_heat"] = float(heat[best])
rec["relative_gap"] = float((heat[idx] - heat[best]) / max(heat[idx], 1e-30))
out.append(rec)
return out
def local_max_margins(indices: Any, heat: Any, height: int, width: int, *, kernel: int = 3) -> np.ndarray:
"""How decisive the reference's local-max decision is for each class-major proposal (the selection metric of a
"decisive" strong-proposal gate): ``(heat[p] - max_neighbour) / heat[p]`` over its same-class ``kernel x kernel``
neighbours in ``heat`` (class-major ``[C * H * W]`` or ``[C, H, W]``, BEFORE the local max). A margin of at most
a few bf16 steps (0.39 % each) is a near-tie that a bf16 device heatmap cannot resolve; 1.0 for a cell without
neighbours. TransFusion gates the strong proposals with a margin above 0.5 % (``strong_overlap_decisive``) and
reports the rest with :func:`near_tie_report`."""
hv = np.asarray(heat, np.float64).reshape(-1)
h, w = int(height), int(width)
cells = h * w
r = int(kernel) // 2
out = []
for idx in np.asarray(indices).astype(np.int64).reshape(-1):
c, p = divmod(int(idx), cells)
y, x = divmod(p, w)
nb = [hv[c * cells + yy * w + xx] for yy in range(y - r, y + r + 1) for xx in range(x - r, x + r + 1)
if (yy, xx) != (y, x) and 0 <= yy < h and 0 <= xx < w]
out.append((hv[idx] - max(nb)) / max(hv[idx], 1e-30) if nb else 1.0)
return np.asarray(out, np.float64)
|