changh95's picture
tt-model push ptv3-p150 (container)
5662e7f verified
Raw History Blame Contribute Delete
14 kB
# SPDX-License-Identifier: Apache-2.0
"""fp32 torch rebuild of the deployed PTv3 v4.0 graphs (encoder, seg head, det head) on the host tables of
``tt_ptv3.host.preprocess`` (identical to what the device consumes), with per-module taps.
Semantics reproduced from the graphs (asserted in ``weights.py``; research/ptv3/SPEC.md 4):
- GELU is the exact erf form; LayerNorm eps 1e-5; BN folded where ``weights.py`` says;
- sub-manifold conv = ``sum_k x[nmap[:, k]] @ W_k`` over the 27 taps of the host neighbour map (missing taps = 0);
- serialized attention: block ``b`` uses order ``b % 2``; ``Npad = ceil(N / 512) * 512``, the sequence row ``j``
is voxel ``order[min(j, N - 1)]`` (pad tokens are copies of the last voxel in curve order, no mask), heads of 16,
``softmax(0.25 * q k^T) v`` per 512-row patch, rows back through ``inverse``;
- pooling: ``proj`` Linear -> segment max over the CSR children -> BN affine -> GELU;
- seg decoder: ``x = GELU(skip(pf_s)) + GELU(up(x))[cluster_s]``; at stages 3 / 2 one block whose CPE reads
``GELU(skip(pf_s))`` (the export quirk), order 0; classifier -> softmax -> first-max argmax;
- det head: fusion, ReLU point projection, BEV scatter-max into a zero 256 x 256 canvas as a gather + max over the
host's per-cell candidate lists (``host.preprocess.bev_index``; exact: max of non-negative values with a zero
row), 12 dense 3x3 convs, heat logits, sigmoid, 3x3 local max on the interior of classes 0-3 with the heat itself
as the seed (border cells and classes 4-6 pass), ``heat * (heat == local_max)``, ONNX-semantics TopK 500 over the
class-major flattening (ties: lower index first; ``topk_indices=`` teacher-forces it), query features + class
encoding, ``query_pos = bev_pos[pos]``, one post-norm decoder layer (self-attention with Q = K = V = query + pos
MLP; cross-attention to the 65,536 BEV keys + key pos MLP), FFN, six prediction heads (``center += query_pos``).
Taps: pass ``taps={}`` (a dict, filled with float32 numpy arrays) and optionally ``tap_filter`` (a callable on the
name); names are listed in :data:`TAP_NAMES` (``{s}`` / ``{b}`` / ``{i}`` expanded).
"""
from __future__ import annotations
from typing import Callable, Dict, List, Optional, Sequence
import numpy as np
from .config import HEAD_DIM, PATCH_SIZE
from .weights import Block, Conv2d, DetHead, Encoder, Linear, MHA, Norm, SegHead, SubmConv
__all__ = ["TAP_NAMES", "encoder_forward", "seg_head_forward", "det_head_forward", "topk_onnx", "ROW_CHUNK",
"PATCH_CHUNK"]
TAP_NAMES = (
"embed", "enc{s}.down", "enc{s}.block{b}.cpe", "enc{s}.block{b}.attn", "enc{s}.block{b}", "point_feat_{s}",
"dec{s}.up", "dec{s}.skip", "dec{s}.sum", "dec{s}.block0", "seg.logits", "seg.probs", "seg.labels",
"det.fusion", "det.point_proj", "det.bev", "det.bev_refine", "det.stem", "det.block{i}", "det.shared",
"det.dense_heatmap", "det.heat", "det.heat_nms", "det.topk_indices", "det.query_feat", "det.query_pos",
"det.query_pos_embed", "det.key_pos_embed", "det.self", "det.norm1", "det.cross", "det.norm2", "det.ffn",
"det.norm3", "det.head.{name}")
ROW_CHUNK = 32768 # rows per sparse-conv gather chunk (host memory only; results do not depend on it)
PATCH_CHUNK = 32 # patches per attention chunk
def _torch():
import torch
return torch
class _Taps:
def __init__(self, taps: Optional[Dict[str, np.ndarray]], tap_filter: Optional[Callable[[str], bool]]):
self.taps, self.filter = taps, tap_filter
def __call__(self, name: str, x):
if self.taps is not None and (self.filter is None or self.filter(name)):
self.taps[name] = x.detach().cpu().numpy().astype(np.float32, copy=True) if hasattr(x, "detach") \
else np.asarray(x)
return x
def _t(a):
torch = _torch()
a = np.ascontiguousarray(a)
return torch.from_numpy(a if a.flags.writeable else a.copy())
def linear(x, p: Linear):
torch = _torch()
return torch.nn.functional.linear(x, _t(p.w), None if p.b is None else _t(p.b))
def layer_norm(x, n: Norm):
torch = _torch()
return torch.nn.functional.layer_norm(x, (x.shape[-1],), _t(n.g), _t(n.b), n.eps)
def gelu(x):
return _torch().nn.functional.gelu(x, approximate="none")
def subm_conv(x, nmap: np.ndarray, conv: SubmConv):
"""Gather-form sub-manifold conv (zero row for missing taps), float32."""
torch = _torch()
n, cin = x.shape
cout = conv.w.shape[0]
xz = torch.cat([x, torch.zeros(1, cin, dtype=x.dtype)], 0)
idx = torch.from_numpy(np.where(nmap >= 0, nmap, n).astype(np.int64))
wmat = _t(conv.w).permute(1, 2, 0).reshape(27 * cin, cout) # tap-major rows k * Cin + j
out = torch.empty(n, cout, dtype=x.dtype)
for r0 in range(0, n, ROW_CHUNK):
g = xz[idx[r0:r0 + ROW_CHUNK]].reshape(-1, 27 * cin)
out[r0:r0 + ROW_CHUNK] = g @ wmat
return out + _t(conv.b)
def serialized_attention(x, blk: Block, order: np.ndarray, inverse: np.ndarray):
"""LN'd input ``x`` -> attention output before ``proj``'s residual (``proj`` applied)."""
torch = _torch()
n, c = x.shape
h = blk.heads
npad = -(-n // PATCH_SIZE) * PATCH_SIZE
seq = torch.from_numpy(np.asarray(order, np.int64)[np.minimum(np.arange(npad), n - 1)])
qkv = linear(x, blk.qkv)[seq].reshape(-1, PATCH_SIZE, 3, h, HEAD_DIM).permute(2, 0, 3, 1, 4)
q, k, v = qkv[0], qkv[1], qkv[2] # [P, H, 512, 16]
out = torch.empty(q.shape[0], PATCH_SIZE, h, HEAD_DIM, dtype=x.dtype)
for p0 in range(0, q.shape[0], PATCH_CHUNK):
sl = slice(p0, p0 + PATCH_CHUNK)
a = torch.softmax((q[sl] * (HEAD_DIM ** -0.5)) @ k[sl].transpose(-2, -1), dim=-1)
out[sl] = (a @ v[sl]).transpose(1, 2)
o = out.reshape(-1, c)[torch.from_numpy(np.asarray(inverse, np.int64))]
return linear(o, blk.proj)
def block_forward(x, blk: Block, nmap, order, inverse, tap: _Taps, cpe_in=None, name: str = ""):
cpe_in = x if cpe_in is None else cpe_in
x = x + layer_norm(linear(subm_conv(cpe_in, nmap, blk.cpe_conv), blk.cpe_linear), blk.cpe_norm)
tap(f"{name}.cpe", x)
x = x + serialized_attention(layer_norm(x, blk.norm1), blk, order, inverse)
tap(f"{name}.attn", x)
x = x + linear(gelu(linear(layer_norm(x, blk.norm2), blk.fc1)), blk.fc2)
return tap(name, x)
def segment_max(x, indptr: np.ndarray):
torch = _torch()
indptr = np.asarray(indptr, np.int64)
lens = torch.from_numpy(indptr[1:] - indptr[:-1])
seg = torch.repeat_interleave(torch.arange(lens.shape[0]), lens)
out = torch.full((lens.shape[0], x.shape[1]), -float("inf"), dtype=x.dtype)
return out.scatter_reduce(0, seg[:, None].expand(-1, x.shape[1]), x[:int(indptr[-1])], reduce="amax",
include_self=True)
def encoder_forward(enc: Encoder, frame, *, taps: Optional[Dict[str, np.ndarray]] = None,
tap_filter: Optional[Callable[[str], bool]] = None) -> List:
"""``frame`` (``host.preprocess.Frame``) -> ``[point_feat_0..4]`` (torch float32)."""
torch = _torch()
tap = _Taps(taps, tap_filter)
x = tap("embed", gelu(linear(torch.from_numpy(frame.voxels.feat), enc.embed)))
outs = []
for s, st in enumerate(enc.stages):
lv = frame.levels[s]
if s > 0:
pooled = segment_max(linear(x, st.down), frame.pools[s - 1].indptr)
x = tap(f"enc{s}.down", gelu(pooled * _t(st.down_norm.scale) + _t(st.down_norm.shift)))
for b, blk in enumerate(st.blocks):
x = block_forward(x, blk, lv.nmap, lv.order[blk.order], lv.inverse[blk.order], tap, name=f"enc{s}.block{b}")
outs.append(tap(f"point_feat_{s}", x))
return outs
def seg_head_forward(seg: SegHead, point_feats: Sequence, frame, *, taps=None, tap_filter=None):
"""-> ``(labels int64 [N0], probs float32 [N0, 15])`` (torch)."""
torch = _torch()
tap = _Taps(taps, tap_filter)
x = point_feats[4]
for s in (3, 2, 1, 0):
d = seg.dec[s]
up = tap(f"dec{s}.up", gelu(linear(x, d.up)))
skip = tap(f"dec{s}.skip", gelu(linear(point_feats[s], d.skip)))
x = tap(f"dec{s}.sum", skip + up[torch.from_numpy(frame.pools[s].cluster)])
if d.block is not None:
lv = frame.levels[s]
x = block_forward(x, d.block, lv.nmap, lv.order[0], lv.inverse[0], tap, cpe_in=skip, name=f"dec{s}.block0")
logits = tap("seg.logits", linear(x, seg.classifier))
probs = tap("seg.probs", torch.softmax(logits, dim=1))
labels = probs.argmax(1)
tap("seg.labels", labels)
return labels, probs
# ---------------------------------------------------------------------------------------------- det head
def conv2d(x, c: Conv2d, residual=None):
torch = _torch()
y = torch.nn.functional.conv2d(x, _t(c.w), _t(c.b), padding=c.padding, dilation=c.dilation)
if residual is not None:
y = y + residual
return torch.relu(y) if c.relu else y
def topk_onnx(scores_flat: np.ndarray, k: int) -> np.ndarray:
"""ONNX ``TopK(largest=1, sorted=1)`` indices with ties broken by the lower index (ORT's order)."""
s = np.asarray(scores_flat, np.float32).reshape(-1)
return np.argsort(-s, kind="stable")[:k].astype(np.int64)
def mha(q_in, kv_in, m: MHA, v_in=None):
"""``[Sq, D]`` x ``[Sk, D]`` -> ``[Sq, D]``: projections, heads, ``softmax((q / div) k^T) v``, out projection."""
torch = _torch()
v_in = kv_in if v_in is None else v_in
hd = m.q.w.shape[0] // m.heads
q = linear(q_in, m.q).reshape(-1, m.heads, hd).transpose(0, 1) / m.scale_div
k = linear(kv_in, m.k).reshape(-1, m.heads, hd).transpose(0, 1)
v = linear(v_in, m.v).reshape(-1, m.heads, hd).transpose(0, 1)
o = torch.empty_like(q)
for h in range(m.heads): # per head: the 500 x 65,536 cross scores stay ~130 MB
o[h] = torch.softmax(q[h] @ k[h].T, dim=-1) @ v[h]
return linear(o.transpose(0, 1).reshape(q_in.shape[0], -1), m.out)
def det_head_forward(det: DetHead, point_feats: Sequence, frame, bev_index: np.ndarray, *,
topk_indices: Optional[np.ndarray] = None, taps=None, tap_filter=None) -> Dict[str, np.ndarray]:
"""-> the 9 outputs of ``ptv3_det3d_head.onnx`` (numpy, ONNX shapes and dtypes). ``bev_index``: the host's
``(65536, 9)`` candidate table whose sentinel is the row count of stage 3 (``host.preprocess.bev_index``)."""
torch = _torch()
tap = _Taps(taps, tap_filter)
pf3, pf4 = point_feats[3], point_feats[4]
n3 = pf3.shape[0]
cluster3 = torch.from_numpy(frame.pools[3].cluster)
fused = tap("det.fusion", gelu(linear(pf3, det.fusion_skip)) + gelu(linear(pf4, det.fusion_proj))[cluster3])
proj = tap("det.point_proj", torch.relu(linear(fused, det.point_proj)))
table = torch.cat([proj, torch.zeros(1, proj.shape[1], dtype=proj.dtype)], 0)
idx = torch.from_numpy(np.where(np.asarray(bev_index) >= n3, n3, bev_index).astype(np.int64))
bev_rows = table[idx].amax(1) # [65536, 128] (empty cells: 0)
tap("det.bev", bev_rows)
size = int(round(bev_rows.shape[0] ** 0.5))
x = bev_rows.reshape(1, size, size, -1).permute(0, 3, 1, 2).contiguous()
x = tap("det.bev_refine", conv2d(x, det.bev_refine))
x = tap("det.stem", conv2d(x, det.stem))
for i, (c1, c2) in enumerate(det.blocks):
x = tap(f"det.block{i}", conv2d(conv2d(x, c1), c2, residual=x))
feat = tap("det.shared", conv2d(x, det.shared)) # [1, 128, H, W]
logits = tap("det.dense_heatmap", conv2d(conv2d(feat, det.heat_hidden), det.heat_out))
heat = tap("det.heat", torch.sigmoid(logits))
local = heat.clone()
pc = list(det.pooled_classes)
local[:, pc, 1:-1, 1:-1] = torch.nn.functional.max_pool2d(heat[:, pc], 3, stride=1, padding=0)
heat_nms = tap("det.heat_nms", heat * (heat == local).to(heat.dtype))
C = heat.shape[1]
cells = size * size
flat = heat_nms.reshape(1, C, cells)
if topk_indices is None:
topk_indices = topk_onnx(flat.reshape(-1).numpy(), det.num_proposals)
top = np.asarray(topk_indices, np.int64).reshape(-1)
tap("det.topk_indices", top)
labels = top // cells
pos = top % cells
pos_t = torch.from_numpy(pos)
feat_flat = feat.reshape(1, feat.shape[1], cells)
onehot = torch.nn.functional.one_hot(torch.from_numpy(labels), C).to(feat.dtype) # [K, C]
query = feat_flat[0][:, pos_t].T + linear(onehot, det.class_encoding) # [K, 128]
tap("det.query_feat", query)
qpos = torch.from_numpy(det.bev_pos)[pos_t] # [K, 2]
tap("det.query_pos", qpos)
qpe = tap("det.query_pos_embed", linear(torch.relu(linear(qpos, det.query_pos[0])), det.query_pos[1]))
kpe = tap("det.key_pos_embed", linear(torch.relu(linear(torch.from_numpy(det.bev_pos), det.key_pos[0])),
det.key_pos[1]))
q = query + qpe
q = tap("det.self", q + mha(q, q, det.self_attn))
q = tap("det.norm1", layer_norm(q, det.norm1))
keys = feat_flat[0].T + kpe # [65536, 128]
q = tap("det.cross", q + mha(q, keys, det.cross_attn))
q = tap("det.norm2", layer_norm(q, det.norm2))
q = tap("det.ffn", q + linear(torch.relu(linear(q, det.ffn[0])), det.ffn[1]))
q = tap("det.norm3", layer_norm(q, det.norm3))
out: Dict[str, np.ndarray] = {}
for name, (h0, h1) in det.heads.items():
y = linear(torch.relu(linear(q, h0)), h1) # [K, c]
if name == "center":
y = y + qpos
tap(f"det.head.{name}", y)
out[name] = y.T.unsqueeze(0).numpy().astype(np.float32)
out["dense_heatmap"] = logits.numpy().astype(np.float32)
out["query_heatmap_score"] = flat[:, :, pos_t].numpy().astype(np.float32)
out["query_labels"] = labels.reshape(1, -1).astype(np.int64)
return out