changh95's picture
tt-model push diffusion-planner-p150 (container)
4d9b003 verified
Raw History Blame Contribute Delete
6.73 kB
# SPDX-License-Identifier: Apache-2.0
"""C19: row gathers with ``ttnn.embedding`` and the gather form of scatter (PLAN.md section 1.2 row C19; probe P4).
A scatter of ``P`` source rows into an ``N``-row destination (a BEV canvas, a to-dense map) is written as a
gather: the host builds ``index[n]`` = the source row of destination row ``n``, or ``sentinel`` (an explicit
all-zero row appended to the table) where nothing lands. Every shape is static, so it traces, and nothing has to
clear the destination first. Probe P4 (common/PROBES.md) measured it bit-exact at the real shapes, with:
- the table ROW_MAJOR bf16 ``[1, 1, V, D]`` (``D % 32 == 0`` for the fused tilized output), UINT32 indices
``[1, N]`` (ROW_MAJOR);
- **the default call** ``ttnn.embedding(index, table, layout=TILE, padding_idx=sentinel,
embeddings_type=PADDED)``: 0.19-0.54 ms per canvas (CenterPoint 480 x 480: 0.21 ms); GENERIC is 5.4-14.6x slower
on canvases (most indices hit the sentinel);
- **PADDED returns the table's own pad row, not zeros** (S:ptv3:304): the zero row must really be in the table
(:func:`with_zero_rows`).
``ttnn.embedding`` is bf16-only; fp32 row gathers are ``ttnn.gather`` (bit-exact but ~1 us per index, probe P13),
not covered here. Indices must lie in ``[0, V)``: an out-of-range index reads outside the table (and can hang the
chip), so :func:`check_index` validates host index arrays before they are uploaded.
numpy only at import; ttnn is imported inside the device functions.
"""
from __future__ import annotations
from typing import Any, Optional, Tuple
import numpy as np
__all__ = [
"check_index",
"gather_rows",
"gather_rows_numpy",
"scatter_rows",
"scatter_rows_numpy",
"with_zero_rows",
]
def check_index(index: Any, *, num_rows: int, sentinel: Optional[int] = None) -> np.ndarray:
"""Validate a host gather index and return it as a contiguous ``uint32`` array of shape ``[1, N]``.
``num_rows`` is the number of real source rows; valid entries are ``0 <= i < num_rows`` or ``i == sentinel``
(the zero row, conventionally ``num_rows``). Raises ``ValueError`` for anything else (negative, too large,
non-integral, non-finite), because an out-of-range index makes the device read outside the table."""
a = np.asarray(index)
if a.dtype.kind == "f":
if not np.all(np.isfinite(a)) or np.any(a != np.floor(a)):
raise ValueError("gather index holds non-integral or non-finite values")
elif a.dtype.kind not in "iu":
raise ValueError(f"gather index must be integral, got dtype {a.dtype}")
flat = a.reshape(-1).astype(np.int64)
valid = (flat >= 0) & (flat < int(num_rows))
if sentinel is not None:
valid |= flat == int(sentinel)
bad = ~valid
if np.any(bad):
i = int(np.flatnonzero(bad)[0])
raise ValueError(f"gather index [{i}] = {int(flat[i])} is outside [0, {num_rows})"
+ ("" if sentinel is None else f" and is not the sentinel {sentinel}"))
return np.ascontiguousarray(flat.astype(np.uint32).reshape(1, -1))
def gather_rows_numpy(index: Any, table: Any) -> np.ndarray:
"""Host oracle: ``out[n] = table[index[n]]`` (``table`` ``[V, D]``)."""
t = np.asarray(table)
return t.reshape(-1, t.shape[-1])[np.asarray(index).reshape(-1).astype(np.int64)]
def scatter_rows_numpy(rows: Any, index: Any, *, sentinel: int) -> np.ndarray:
"""Host oracle of :func:`scatter_rows`: ``rows`` ``[P, D]`` with a zero row appended at ``sentinel``
(``sentinel >= P``; rows between ``P`` and ``sentinel`` are zero too), gathered by ``index``."""
r = np.asarray(rows, dtype=np.float32)
r = r.reshape(-1, r.shape[-1])
table = np.zeros((max(int(sentinel) + 1, r.shape[0]), r.shape[1]), np.float32)
table[: r.shape[0]] = r
return gather_rows_numpy(index, table)
def _rm(t: Any):
import ttnn
return ttnn.to_layout(t, ttnn.ROW_MAJOR_LAYOUT) if t.layout != ttnn.ROW_MAJOR_LAYOUT else t
def with_zero_rows(rows: Any, count: int = 1):
"""A ROW_MAJOR gather table ``[1, 1, P + count, D]``: the device rows ``rows`` (``[1, 1, P, D]``, TILE or
ROW_MAJOR, bf16) followed by ``count`` zero rows (the sentinel rows of :func:`scatter_rows`). One ``ttnn.pad``
on the device (plus an untilize for a TILE input); traceable."""
import ttnn
if count < 1:
raise ValueError("count must be >= 1")
shape = tuple(int(s) for s in rows.shape)
if len(shape) != 4 or shape[0] != 1 or shape[1] != 1:
raise ValueError(f"rows must be [1, 1, P, D], got {shape}")
rm = _rm(rows)
return ttnn.pad(rm, [(0, 0), (0, 0), (0, int(count)), (0, 0)], 0.0)
def gather_rows(index: Any, table: Any, *, sentinel: Optional[int] = None, layout: str = "tile",
memory_config: Any = None):
"""``out[0, 0, n] = table[0, 0, index[0, n]]`` on the device -> ``[1, 1, N, D]``.
``index``: UINT32 ROW_MAJOR ``[1, N]`` device tensor; ``table``: bf16 ROW_MAJOR ``[1, 1, V, D]`` (a TILE table
is untilized by ttnn first: one extra program). ``sentinel``: the index of an all-zero table row; given, the
call is P4's fast PADDED form (the table must really hold zeros there). ``layout="tile"`` (default) writes
the tilized output directly when ``N % 32 == 0`` and ``D % 32 == 0``."""
import ttnn
lay = ttnn.TILE_LAYOUT if layout == "tile" else ttnn.ROW_MAJOR_LAYOUT
kw = {"layout": lay}
if sentinel is not None:
kw.update(padding_idx=int(sentinel), embeddings_type=ttnn.EmbeddingsType.PADDED)
if memory_config is not None:
kw["memory_config"] = memory_config
out = ttnn.embedding(index, table, **kw) # [1, N, D]
n, d = int(out.shape[-2]), int(out.shape[-1])
return ttnn.reshape(out, (1, 1, n, d))
def scatter_rows(rows: Any, index: Any, *, sentinel: Optional[int] = None, layout: str = "tile",
memory_config: Any = None) -> Tuple[Any, Any]:
"""Gather-form scatter: ``canvas[n] = rows[index[n]]``, or zeros where ``index[n] == sentinel``.
``rows``: ``[1, 1, P, D]`` device rows (TILE or ROW_MAJOR bf16); ``sentinel`` defaults to ``P`` (the zero
row appended by :func:`with_zero_rows`). Returns ``(canvas, table)``: the ``[1, 1, N, D]`` canvas (TILE by
default, e.g. the NHWC input of a conv) and the table, which the caller may deallocate."""
p = int(rows.shape[-2])
sentinel = p if sentinel is None else int(sentinel)
if sentinel < p:
raise ValueError(f"sentinel {sentinel} must be >= the number of source rows {p}")
table = with_zero_rows(rows, sentinel - p + 1)
return gather_rows(index, table, sentinel=sentinel, layout=layout, memory_config=memory_config), table