File size: 12,075 Bytes
4d9b003 | 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 | # SPDX-License-Identifier: Apache-2.0
"""C22: heatmap peaks of the query-based LiDAR detectors -- sigmoid and the 3x3 local maximum (PLAN.md section 1.2
row C22; probe P6, common/PROBES.md).
TransFusion, BEVFusion and PTv3 keep a heatmap cell only where it is the maximum of its 3x3 neighbourhood, with
model-specific border and class rules:
- ``"transfusion"`` (TF, S:transfusion:169): ``MaxPool 3x3 s1 p0`` padded back with a ZERO border, every class: the
border cells are always suppressed;
- ``"bevfusion"`` (BF, S:bevfusion:258): classes ``pooled_classes`` (default 0-3) as TF; the other classes are not
pooled (``local_max = heat``): every cell passes;
- ``"ptv3"`` (PT, S:ptv3:217): ``local_max`` starts as the heat itself and only the interior of the pooled classes is
replaced by the pool: border cells and the other classes pass.
All three are one exact masked chain (P6, bit-exact on the device, TF 95 / BF 85 / PT 145 us):
keep = m_pass + (heat == max_pool2d(heat, 3x3, stride 1, padding 1)) * m_eq out = heat * keep
with two constant masks ``m_eq`` / ``m_pass`` (disjoint, values 0 / 1, laid out as the NHWC rows ``[H*W, C]`` of the
heatmap). The pool's padding never matters: a padded window only covers border cells, where ``m_eq`` is 0. Not the
literal ``p0`` pool + pad form (222 us for TF and two more programs). ``ttnn.max_pool2d`` is bf16 only (FLOAT32 is
rejected, ``pool_op.cpp:30``), so the heat is bf16: :func:`sigmoid_heat` takes fp32 logits through an fp32 sigmoid and
rounds once.
Device layout: the heatmap is a TILE ``[1, 1, H*W, C]`` tensor (what a conv head writes); the result has the same
layout (the input of C21's class-major top-k row and of the query-heat gather of C26).
numpy only at import; ttnn is imported inside the device functions.
"""
from __future__ import annotations
from typing import Any, Dict, Optional, Sequence, Tuple
import numpy as np
__all__ = ["RULES", "DEFAULT_POOLED_CLASSES", "local_max_masks", "local_max_numpy", "local_max_literal_numpy",
"sigmoid_heat", "sigmoid_heat_numpy", "LocalMax"]
RULES = ("transfusion", "bevfusion", "ptv3")
DEFAULT_POOLED_CLASSES = 4 # BF / PT: classes 0-3 pooled (the last class(es) pass)
def _pooled(classes: int, rule: str, pooled_classes: Optional[Sequence[int]]) -> np.ndarray:
if rule not in RULES:
raise ValueError(f"rule {rule!r}: expected one of {RULES}")
if pooled_classes is None:
pooled_classes = range(classes) if rule == "transfusion" else range(min(DEFAULT_POOLED_CLASSES, classes))
p = np.zeros(classes, np.float32)
for c in pooled_classes:
if not 0 <= int(c) < classes:
raise ValueError(f"pooled class {c} outside [0, {classes})")
p[int(c)] = 1.0
if rule == "transfusion" and not p.all():
raise ValueError("the transfusion rule pools every class")
return p
def local_max_masks(height: int, width: int, classes: int, *, rule: str = "transfusion",
pooled_classes: Optional[Sequence[int]] = None, kernel: int = 3) -> Tuple[np.ndarray, np.ndarray]:
"""The constant masks ``(m_pass, m_eq)`` of the chain for ``rule`` (module docstring), float32 NHWC rows
``[height * width, classes]``. ``kernel`` (odd): the pool size; the border is ``kernel // 2`` cells wide."""
if kernel < 1 or kernel % 2 == 0:
raise ValueError(f"kernel must be odd and positive, got {kernel}")
r = kernel // 2
if height <= 2 * r or width <= 2 * r:
raise ValueError(f"a {height}x{width} map has no interior for a {kernel}x{kernel} pool")
pooled = _pooled(classes, rule, pooled_classes)
interior = np.zeros((height, width, 1), np.float32)
interior[r:height - r, r:width - r] = 1.0
m_eq = interior * pooled[None, None, :]
if rule == "transfusion":
m_pass = np.zeros_like(m_eq)
elif rule == "bevfusion":
m_pass = np.broadcast_to(1.0 - pooled[None, None, :], m_eq.shape).astype(np.float32)
else:
m_pass = 1.0 - m_eq
return (np.ascontiguousarray(m_pass.reshape(height * width, classes)),
np.ascontiguousarray(m_eq.reshape(height * width, classes)))
def _pool_numpy(heat_nhwc: np.ndarray, height: int, width: int, kernel: int) -> np.ndarray:
"""3x3 (k x k) stride-1 max pool with ``-inf`` padding of NHWC rows ``[H*W, C]`` (exact on any float data)."""
c = heat_nhwc.shape[-1]
x = np.asarray(heat_nhwc, np.float64).reshape(height, width, c)
r = kernel // 2
p = np.full((height + 2 * r, width + 2 * r, c), -np.inf)
p[r:r + height, r:r + width] = x
out = np.full_like(x, -np.inf)
for dy in range(kernel):
for dx in range(kernel):
np.maximum(out, p[dy:dy + height, dx:dx + width], out=out)
return out.reshape(height * width, c)
def local_max_numpy(heat_nhwc: Any, height: int, width: int, m_pass: Any, m_eq: Any, kernel: int = 3) -> np.ndarray:
"""Host oracle of the masked chain on NHWC rows ``[H*W, C]`` (float32 out; exact on bf16-valued input)."""
h = np.asarray(heat_nhwc, np.float32).reshape(height * width, -1)
pooled = _pool_numpy(h, height, width, kernel)
keep = np.asarray(m_pass, np.float32) + (h == pooled).astype(np.float32) * np.asarray(m_eq, np.float32)
return (h * keep).astype(np.float32)
def local_max_literal_numpy(heat_nchw: Any, rule: str = "transfusion", *,
pooled_classes: Optional[Sequence[int]] = None, kernel: int = 3) -> np.ndarray:
"""The models' literal forms (``heat * (heat == local_max)``, NCHW ``(C, H, W)`` or ``(1, C, H, W)``), the oracle
the masked chain is proven against: TF ``max_pool p0`` + zero border (all classes); BF pooled classes as TF and
the others ``local_max = heat``; PT ``local_max = heat`` with the interior of the pooled classes pooled."""
x = np.asarray(heat_nchw, np.float32)
squeeze = x.ndim == 4
if squeeze:
x = x[0]
c, h, w = x.shape
pooled = _pooled(c, rule, pooled_classes).astype(bool)
r = kernel // 2
full = _pool_numpy(x.reshape(c, h * w).T, h, w, kernel).T.reshape(c, h, w)
interior = np.zeros((h, w), bool)
interior[r:h - r, r:w - r] = True
if rule == "transfusion":
local = np.where(interior[None], full, 0.0)
elif rule == "bevfusion":
local = np.where(pooled[:, None, None], np.where(interior[None], full, 0.0), x)
else:
local = np.where(pooled[:, None, None] & interior[None], full, x)
out = (x * (x == local)).astype(np.float32)
return out[None] if squeeze else out
def sigmoid_heat_numpy(logits: Any, dtype: str = "bfloat16") -> np.ndarray:
"""Host model of :func:`sigmoid_heat`: float64 sigmoid of the float32 logits, rounded once to ``dtype`` (bf16 RNE)."""
s = (1.0 / (1.0 + np.exp(-np.asarray(logits, np.float64)))).astype(np.float32)
if dtype in ("bfloat16", "bf16"):
from ..tensors import round_to_bf16
return round_to_bf16(s)
return s
def sigmoid_heat(logits: Any, *, dtype: str = "bfloat16", memory_config: Any = None):
"""Device ``sigmoid(logits)`` computed in the logits' dtype (fp32 logits: an fp32 sigmoid) and cast once to
``dtype`` (bf16: what ``max_pool2d`` and C21's ``topk_large_indices`` take). Returns ``(heat, sig)``: the cast
heat and the sigmoid before the cast (the same tensor when no cast is needed)."""
import ttnn
from ..tensors import ttnn_dtype
kw = {} if memory_config is None else {"memory_config": memory_config}
sig = ttnn.sigmoid(logits, **kw)
want = ttnn_dtype(dtype)
heat = sig if sig.dtype == want else ttnn.typecast(sig, want)
return heat, sig
def _mc(spec: Any):
import ttnn
if spec is None or spec == "dram":
return ttnn.DRAM_MEMORY_CONFIG
if spec == "l1":
return ttnn.L1_MEMORY_CONFIG
return spec
class LocalMax:
"""The device chain for one heatmap geometry: ``heat`` (bf16 TILE ``[1, 1, H*W, C]``) -> ``heat * keep``.
The masks are uploaded (bf16 TILE, exact 0 / 1) by the first call, outside any capture; with the TF rule
(``m_pass`` = 0) the chain is ``max_pool2d``, ``eq``, ``multiply`` (keep = eq * m_eq), ``multiply``; otherwise
``addcmul(m_pass, eq, m_eq)`` replaces the first multiply. ``memory_config``: where the pool and the masks live
(``"dram"`` default; ``"l1"`` is P6's 95 us form for maps that fit)."""
def __init__(self, height: int, width: int, classes: int, *, rule: str = "transfusion",
pooled_classes: Optional[Sequence[int]] = None, kernel: int = 3, memory_config: Any = "dram",
name: str = "local_max"):
self.height, self.width, self.classes = int(height), int(width), int(classes)
self.rule = rule
self.kernel = int(kernel)
self.name = name
self.m_pass, self.m_eq = local_max_masks(self.height, self.width, self.classes, rule=rule,
pooled_classes=pooled_classes, kernel=self.kernel)
self.passes = bool(self.m_pass.any())
self.memory_config = memory_config
self._dev: Optional[Dict[str, Any]] = None
@property
def cells(self) -> int:
return self.height * self.width
def _upload(self, device: Any) -> Dict[str, Any]:
import torch
import ttnn
def up(a: np.ndarray):
t = torch.from_numpy(np.ascontiguousarray(a.reshape(1, 1, self.cells, self.classes)))
return ttnn.from_torch(t, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT, device=device,
memory_config=_mc(self.memory_config))
self._dev = {"m_eq": up(self.m_eq), "m_pass": up(self.m_pass) if self.passes else None}
return self._dev
def __call__(self, heat: Any):
import ttnn
shape = tuple(int(s) for s in heat.shape)
if shape != (1, 1, self.cells, self.classes):
raise ValueError(f"{self.name}: heat must be [1, 1, {self.cells}, {self.classes}], got {shape}")
if heat.dtype != ttnn.bfloat16:
raise ValueError(f"{self.name}: max_pool2d takes bf16 only (got {heat.dtype}): use sigmoid_heat")
dev = self._dev or self._upload(heat.device())
r = self.kernel // 2
pooled = ttnn.max_pool2d(input_tensor=heat, batch_size=1, input_h=self.height, input_w=self.width,
channels=self.classes, kernel_size=[self.kernel, self.kernel], stride=[1, 1],
padding=[r, r], dilation=[1, 1], ceil_mode=False,
memory_config=_mc(self.memory_config), output_layout=ttnn.TILE_LAYOUT)
if tuple(int(s) for s in pooled.shape) != shape:
full = pooled
pooled = ttnn.slice(full, [0, 0, 0, 0], list(shape))
ttnn.deallocate(full, False)
eq = ttnn.eq(heat, pooled)
ttnn.deallocate(pooled, False)
if self.passes:
keep = ttnn.addcmul(dev["m_pass"], eq, dev["m_eq"])
else:
keep = ttnn.multiply(eq, dev["m_eq"])
ttnn.deallocate(eq, False)
out = ttnn.multiply(heat, keep)
ttnn.deallocate(keep, False)
return out
def numpy(self, heat_nhwc: Any) -> np.ndarray:
"""Host oracle of this chain on NHWC rows (bf16-valued input gives the device result bit for bit)."""
return local_max_numpy(heat_nhwc, self.height, self.width, self.m_pass, self.m_eq, self.kernel)
def release(self) -> None:
import ttnn
if self._dev is not None:
for t in self._dev.values():
if t is not None and t.is_allocated():
ttnn.deallocate(t)
self._dev = None
def describe(self) -> Dict[str, Any]:
return {"name": self.name, "grid": [self.height, self.width], "classes": self.classes, "rule": self.rule,
"kernel": self.kernel, "pooled_classes": [int(c) for c in np.flatnonzero(self.m_eq.max(axis=0))],
"passes": self.passes, "memory_config": str(self.memory_config)}
|