File size: 10,396 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 | # SPDX-License-Identifier: Apache-2.0
"""C23: ``ttnn.grid_sample`` helpers (PLAN.md section 1.2 row C23; probe P9, common/PROBES.md; first built by the
BEVDet port for AlignBEV and FPN_LSS).
Rules from probe P9 (``common/probes/g4-attention-gridsample-msda.md``), enforced or encoded here:
- **fp32 grids** for every data-dependent grid (bf16 grids cost PCC 0.9988 at 53 px and 0.992 at 128 px);
- **input C % 32 == 0** (C = 80 is rejected; zero pad channels stay exactly 0): :func:`padded_channels`,
:func:`pad_channels`;
- **per-batch grids**: grid N must equal input N (no broadcast over the batch);
- **ROW_MAJOR, interleaved (or height-sharded)** input and grid, ``padding_mode="zeros"`` only;
- **never a FLOAT32 input together with ``fp32_dest_acc_en``**: wrong, non-deterministic output (strict xfail in P9);
:func:`grid_sample` refuses it;
- **exact integer coordinates**: the kernel recovers pixel coordinates as ``x = g * W/2 + (W - 1)/2`` in fp32
(``grid_sample_reader_common.hpp``, align_corners=False), so the align_corners=False normalisation
``g = (2x + 1) / W - 1`` returns integer x exactly whenever ``(2x + 1) / W`` is exact in fp32 (``W`` a power of
two): an identity warp is then a bit-exact copy (P9: the literal align_corners=True normalisation leaves 2.8-7.6 %
of values off by up to 1/32). Zero padding acts in pixel space, so the normalisation choice does not change what
is sampled: callers state pixel coordinates (:func:`affine_grid`, :func:`resize_grid`) and always call the op with
``align_corners=False``;
- the four bilinear weights are computed in fp32 and **truncated** (not rounded) to bf16 by the kernel: a 0.33-0.6 %
rrmse floor for fractional coordinates (plan risk R3; K5 with fp32 weights removes it). :func:`emulate_grid_sample`
reproduces that arithmetic on the host (the device test's oracle).
Host functions are numpy only (grids are built on the host, per frame or once, and uploaded as fp32 ROW_MAJOR
tensors); :func:`grid_sample` is the device call with the checks above. DCNv2 as one grid_sample with K = 9
(``batch_output_channels=True``, BEVFormer / METEOR) and the fused MSDA wrapper are added by their ports.
"""
from __future__ import annotations
from typing import Any, Optional, Sequence, Tuple
import numpy as np
__all__ = ["OUTSIDE", "padded_channels", "pad_channels", "pixel_to_grid", "kernel_pixels", "affine_pixel_coords",
"affine_grid", "resize_pixel_coords", "resize_grid", "emulate_grid_sample", "grid_sample"]
F32 = np.float32
# A normalised coordinate whose four taps fall outside any input (pixel -1.5 W - 0.5): the sample is exactly 0.
OUTSIDE = F32(-4.0)
def padded_channels(c: int, multiple: int = 32) -> int:
"""The channel count ``grid_sample`` accepts for ``c`` real channels (the next multiple of 32)."""
return -(-int(c) // int(multiple)) * int(multiple)
def pad_channels(x: np.ndarray, multiple: int = 32) -> np.ndarray:
"""Zero-pad the last (channel) dimension of an NHWC array to a multiple of ``multiple`` (no copy when aligned)."""
x = np.asarray(x)
c = x.shape[-1]
cp = padded_channels(c, multiple)
if cp == c:
return x
out = np.zeros(x.shape[:-1] + (cp,), x.dtype)
out[..., :c] = x
return out
def pixel_to_grid(px: Any, size: int) -> np.ndarray:
"""Pixel coordinate (0 = centre of the first pixel) -> normalised coordinate for ``align_corners=False``:
``(2 px + 1) / size - 1`` in float32 (exact for integer ``px`` when ``size`` is a power of two)."""
p = np.asarray(px, F32)
return (F32(2.0) * p + F32(1.0)) / F32(size) - F32(1.0)
def kernel_pixels(g: Any, size: int) -> np.ndarray:
"""The pixel coordinate the device kernel recovers from a normalised coordinate (align_corners=False):
``g * size/2 + (size - 1)/2`` in float32."""
g = np.asarray(g, F32)
return g * F32(size / 2.0) + F32((size - 1) / 2.0)
def affine_pixel_coords(transforms: np.ndarray, out_hw: Tuple[int, int]) -> Tuple[np.ndarray, np.ndarray]:
"""Per batch n and output pixel (h, w): ``ix = a w + b h + c``, ``iy = d w + e h + f`` in float32 with one
rounding per operation (``transforms`` [N, 6] = (a, b, c, d, e, f), the BEVDet AlignBEV / BEVFormer-style pixel
affine) -> (ix, iy) [N, H, W]."""
t = np.asarray(transforms, F32).reshape(-1, 6)
h, w = out_hw
hh, ww = np.meshgrid(np.arange(h, dtype=F32), np.arange(w, dtype=F32), indexing="ij")
a, b, c, d, e, f = (t[:, k, None, None] for k in range(6))
ix = (a * ww + b * hh) + c
iy = (d * ww + e * hh) + f
return ix.astype(F32), iy.astype(F32)
def affine_grid(transforms: np.ndarray, in_hw: Tuple[int, int], out_hw: Optional[Tuple[int, int]] = None, *,
valid: Optional[Sequence[bool]] = None) -> np.ndarray:
"""fp32 grid [N, H, W, 2] (x, y) sampling an ``in_hw`` input at :func:`affine_pixel_coords` (align_corners=False
normalisation: identity transforms are bit-exact copies for power-of-two input sizes). Batches with
``valid[n] == False`` sample :data:`OUTSIDE` (exact zeros, e.g. a history slot that holds no frame yet)."""
out_hw = tuple(in_hw) if out_hw is None else tuple(out_hw)
ix, iy = affine_pixel_coords(transforms, out_hw)
grid = np.stack([pixel_to_grid(ix, in_hw[1]), pixel_to_grid(iy, in_hw[0])], axis=-1).astype(F32)
if valid is not None:
v = np.asarray(valid, bool).reshape(-1)
if v.size != grid.shape[0]:
raise ValueError(f"valid has {v.size} entries for {grid.shape[0]} batches")
grid[~v] = OUTSIDE
return np.ascontiguousarray(grid)
def resize_pixel_coords(in_size: int, out_size: int, align_corners: bool = True) -> np.ndarray:
"""Source pixel of every output pixel of a 1-D bilinear resize (float64), torch's ``F.interpolate`` rules:
align_corners=True ``i (in - 1) / (out - 1)``; False ``(i + 0.5) in / out - 0.5`` (not clamped: the zero padding
of grid_sample differs from torch's edge clamp there, so prefer C18 ``Resize2d`` for align_corners=False)."""
i = np.arange(out_size, dtype=np.float64)
if align_corners:
return i * (in_size - 1) / max(out_size - 1, 1)
return (i + 0.5) * in_size / out_size - 0.5
def resize_grid(in_hw: Tuple[int, int], out_hw: Tuple[int, int], *, align_corners: bool = True,
batch: int = 1) -> np.ndarray:
"""Static fp32 grid [batch, Ho, Wo, 2] of a bilinear resize (source pixels in float64, normalised with the
align_corners=False formula; call :func:`grid_sample`). For align_corners=True every source pixel lies inside
the input, so the result equals ``F.interpolate(..., align_corners=True)`` up to the bf16 weight truncation."""
sy = resize_pixel_coords(in_hw[0], out_hw[0], align_corners)
sx = resize_pixel_coords(in_hw[1], out_hw[1], align_corners)
gx = ((2.0 * sx + 1.0) / in_hw[1] - 1.0).astype(F32)
gy = ((2.0 * sy + 1.0) / in_hw[0] - 1.0).astype(F32)
grid = np.empty((out_hw[0], out_hw[1], 2), F32)
grid[..., 0] = gx[None, :]
grid[..., 1] = gy[:, None]
return np.ascontiguousarray(np.broadcast_to(grid, (int(batch),) + grid.shape))
def _truncate_bf16(a: np.ndarray) -> np.ndarray:
bits = np.ascontiguousarray(a, F32).view(np.uint32) & np.uint32(0xFFFF0000)
return bits.view(F32)
def emulate_grid_sample(x: np.ndarray, grid: np.ndarray) -> np.ndarray:
"""Host emulation of the device's bilinear ``grid_sample`` (align_corners=False, zero padding): pixel recovery
as :func:`kernel_pixels`, ``floor``, fp32 fractions and weights truncated to bf16, taps outside the input skipped.
``x`` [N, H, W, C], ``grid`` [N, Ho, Wo, 2] -> [N, Ho, Wo, C] float64 (the device sums the four bf16 products
in its own order, so compare with a tolerance; an exact identity / outside sample is exact on both)."""
x = np.asarray(x, np.float64)
g = np.asarray(grid, F32)
n, h, w, _ = x.shape
px = kernel_pixels(g[..., 0], w)
py = kernel_pixels(g[..., 1], h)
x0 = np.floor(px).astype(np.int64)
y0 = np.floor(py).astype(np.int64)
fx = (px - x0.astype(F32)).astype(F32)
fy = (py - y0.astype(F32)).astype(F32)
one = F32(1.0)
out = np.zeros(g.shape[:-1] + (x.shape[-1],), np.float64)
bidx = np.arange(n)[:, None, None]
for dy, wy in ((0, one - fy), (1, fy)):
for dx, wx in ((0, one - fx), (1, fx)):
yy, xx = y0 + dy, x0 + dx
ok = (yy >= 0) & (yy < h) & (xx >= 0) & (xx < w)
wgt = _truncate_bf16((wy * wx).astype(F32)).astype(np.float64) * ok
vals = x[bidx, np.clip(yy, 0, h - 1), np.clip(xx, 0, w - 1)]
out += vals * wgt[..., None]
return out
def grid_sample(x: Any, grid: Any, *, compute_kernel_config: Any = None, memory_config: Any = None,
batch_output_channels: bool = False):
"""``ttnn.grid_sample(x, grid)``: bilinear, zero padding, ``align_corners=False`` (pair it with
:func:`pixel_to_grid` / :func:`affine_grid` / :func:`resize_grid`), with the P9 rules checked before the call.
The output goes to DRAM by default."""
import ttnn
if x.layout != ttnn.ROW_MAJOR_LAYOUT or grid.layout != ttnn.ROW_MAJOR_LAYOUT:
raise ValueError("grid_sample needs ROW_MAJOR input and grid")
if int(x.padded_shape[-1]) % 32:
raise ValueError(f"grid_sample input channels must be a multiple of 32, got {x.padded_shape[-1]} "
"(pad them: deform.pad_channels)")
if int(x.shape[0]) != int(grid.shape[0]):
raise ValueError(f"grid batch {grid.shape[0]} != input batch {x.shape[0]} (expand the grid per batch)")
if grid.dtype != ttnn.float32:
raise ValueError("use an fp32 grid for data-dependent sampling (probe P9: bf16 grids lose PCC)")
if x.dtype == ttnn.float32 and compute_kernel_config is not None and \
getattr(compute_kernel_config, "fp32_dest_acc_en", False):
raise ValueError("a FLOAT32 input with fp32_dest_acc_en returns garbage (probe P9 defect)")
return ttnn.grid_sample(x, grid, mode="bilinear", padding_mode="zeros", align_corners=False,
use_precomputed_grid=False, batch_output_channels=batch_output_channels,
memory_config=ttnn.DRAM_MEMORY_CONFIG if memory_config is None else memory_config,
compute_kernel_config=compute_kernel_config)
|