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)