File size: 21,956 Bytes
be62f78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# SPDX-License-Identifier: Apache-2.0
"""C13 sparse rulebooks: spconv-faithful neighbour maps of sparse 3-D convolutions, built on the host, in the
*gather* form the device consumes (PLAN.md section 1.2 row C13; owned by the BEVFusion port; S:bevfusion:465-466,
S:ptv3:158).

A sparse tensor is a set of active voxels ``coords (N, 3)`` = ``(x, y, z)`` (batch 0 implied) in a grid of
``spatial_shape (X, Y, Z)``. Every sparse conv of the Autoware graphs (``autoware::GetIndicePairsImplicitGemm`` +
``ImplicitGemm``, spconv 2.3.8 ``MaskImplicitGemm``) is, per output voxel ``o``,

    out[o] = sum_k  W[:, k, :] @ in[ nmap[o, k] ]          (taps with ``nmap[o, k] == -1`` contribute nothing)

with ``W`` the KRSC filter ``[Cout, k0, k1, k2, Cin]`` (``k0`` along x, ``k1`` along y, ``k2`` along z) and the tap
index ``k = (a * k1 + b) * k2 + c`` (row-major over (x, y, z), spconv's ``layout_rs``). This module builds
``nmap`` -- a pure gather table, no scatter, for sub-manifold and strided layers alike -- and the device-side index
forms of it:

- :class:`SparseLevel`: the active set of one resolution with a hashed lookup (sorted int64 keys + ``searchsorted``;
  keys are exact for any coordinate, also outside the grid);
- :func:`subm_neighbor_map`: sub-manifold conv (output set == input set; spconv uses ``padding = (k // 2) *
  dilation`` whatever the attribute says, ``spconv/csrc/sparse/indices.py:1522``). ``rule="spconv"`` (default)
  reproduces spconv's **13-query + symmetric-write** construction (``generate_subm_conv_inds`` launches only the taps
  ``k <= K // 2``, ``indices.py:1532``; a hit also writes the mirrored pair ``pair[K-1-k][in] = out``, ``:865``; the
  centre is always the voxel itself, ``:838-845``). Its bounds check is applied to the *queried* coordinate, so a tap
  ``k < K // 2`` is accepted only if the **neighbour** lies inside the grid and a tap ``k > K // 2`` only if the
  **output voxel** does (the asymmetric-bounds rule of S:ptv3:158, verified bit-exact against spconv there). For
  voxel sets inside the grid (every BEVFusion level) it equals ``rule="exact"``, the plain 27-tap lookup;
- :func:`strided_neighbor_map`: regular (strided) sparse conv: output ``o`` is active iff some active input ``i``
  and tap ``k`` give ``i = o * s - p + k * d`` with ``0 <= o < out_shape`` (spconv ``generate_conv_inds``); outputs
  come in ascending linear-key order (any order is equivalent: the next layers are permutation-equivariant); one
  input per (output, tap), so the strided layer is a gather too;
- device forms (C28, probe P4): :func:`gather_index` (``-1`` -> an explicit zero sentinel row, capacity padding),
  :func:`im2col_index` (the **tile-ordered** index of the one-gather im2col: ``(voxel block of 32, tap, voxel)``,
  so ``ttnn.embedding(..., layout=TILE)`` of a ``[N/32, K * 32]`` index into a ``Cin % 32 == 0`` table gives a tensor
  whose tile sequence *is* the ``[N, K * Cin]`` im2col matrix, read through ``ttnn.experimental.view``) and its
  oracle :func:`im2col_numpy`; :func:`dense_gather_index` (to-dense as a gather: one ``[X * Y]`` canvas index per z
  slice);
- capacities (PLAN.md 0.2: grid-independent constants chosen from data): :func:`select_capacity` picks the smallest
  bucket that holds a count, and flags an overflow;
- oracles: :func:`sparse_conv_numpy` (the gather form above, float64 accumulation) and :func:`dense_conv3d_numpy` (a
  brute-force dense 3-D cross-correlation restricted to the active outputs; spconv == dense Conv3d on the active set).

Not here yet (their consumer adds them, PTv3): the linear-key **alias** mode of S:ptv3:388-393 (spconv inserts
out-of-grid voxels under aliasing int32 keys; ``rule="alias"`` raises ``NotImplementedError``) and Morton /
serialization helpers.

numpy only; no side effects on import.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Dict, Iterable, Optional, Sequence, Tuple

import numpy as np

__all__ = [
    "SparseLevel",
    "RULES",
    "kernel_offsets",
    "conv_out_shape",
    "subm_neighbor_map",
    "strided_neighbor_map",
    "pairs_count",
    "gather_index",
    "im2col_index",
    "im2col_numpy",
    "dense_gather_index",
    "dense_numpy",
    "select_capacity",
    "sparse_conv_numpy",
    "dense_conv3d_numpy",
    "TILE_ROWS",
    "neighbor_stats",
    "iter_taps",
]

RULES = ("spconv", "exact")      # subm neighbour rules ("alias": PTv3, not implemented yet)
TILE_ROWS = 32                   # rows of one ttnn tile: the voxel block of the tile-ordered im2col index


def _triple(v: Any, name: str) -> Tuple[int, int, int]:
    if isinstance(v, (int, np.integer)):
        t = (int(v),) * 3
    else:
        t = tuple(int(x) for x in v)
    if len(t) != 3:
        raise ValueError(f"{name} needs 3 values, got {v!r}")
    return t  # type: ignore[return-value]


# ------------------------------------------------------------------------------------------------- levels

@dataclass
class SparseLevel:
    """The active voxels of one resolution: ``coords`` int64 ``(N, 3)`` ``(x, y, z)`` in ``spatial_shape (X, Y, Z)``.

    Rows are the feature rows of the level (row ``i`` holds voxel ``coords[i]``). Coordinates outside the grid are
    allowed (spconv carries them, S:ptv3:158); keys are exact for them (no aliasing). Duplicate coordinates are
    refused (a spconv indice set is unique)."""

    coords: np.ndarray
    spatial_shape: Tuple[int, int, int]

    def __post_init__(self) -> None:
        c = np.asarray(self.coords)
        if c.ndim != 2 or c.shape[1] != 3:
            raise ValueError(f"coords must be (N, 3) (x, y, z), got {c.shape}")
        if c.size and not np.issubdtype(c.dtype, np.integer):
            raise ValueError(f"coords must be integers, got {c.dtype}")
        self.coords = np.ascontiguousarray(c, dtype=np.int64)
        self.spatial_shape = _triple(self.spatial_shape, "spatial_shape")
        if min(self.spatial_shape) <= 0:
            raise ValueError(f"spatial_shape must be positive, got {self.spatial_shape}")
        keys = self.keys_of(self.coords)
        order = np.argsort(keys, kind="stable")
        self._sorted = keys[order]
        self._perm = order
        if len(self._sorted) > 1 and np.any(self._sorted[1:] == self._sorted[:-1]):
            raise ValueError("coords hold duplicate voxels")

    # exact keys over the bounding box of the grid and the voxels, widened by a margin, so no coordinate aliases
    _MARGIN = 8

    def _box(self) -> Tuple[np.ndarray, np.ndarray]:
        box = getattr(self, "_box_cache", None)
        if box is None:
            shape = np.asarray(self.spatial_shape, dtype=np.int64)
            lo, hi = np.zeros(3, np.int64), shape.copy()
            if len(self.coords):
                lo = np.minimum(lo, self.coords.min(axis=0))
                hi = np.maximum(hi, self.coords.max(axis=0) + 1)
            box = (lo - self._MARGIN, hi + self._MARGIN)
            self._box_cache = box
        return box

    def keys_of(self, q: np.ndarray) -> np.ndarray:
        """Exact int64 keys of ``q (M, 3)``; coordinates outside the widened bounding box get key -1 (never found:
        no voxel of the level lies there)."""
        q = np.asarray(q, dtype=np.int64).reshape(-1, 3)
        lo, hi = self._box()
        dims = hi - lo
        ok = np.all((q >= lo) & (q < hi), axis=1)
        r = q - lo
        k = (r[:, 0] * dims[1] + r[:, 1]) * dims[2] + r[:, 2]
        return np.where(ok, k, -1)

    @property
    def num_active(self) -> int:
        return int(len(self.coords))

    def in_grid(self, q: Optional[np.ndarray] = None) -> np.ndarray:
        """``0 <= q < spatial_shape`` on every axis (default: the level's own coords)."""
        q = self.coords if q is None else np.asarray(q, dtype=np.int64)
        return np.all((q >= 0) & (q < np.asarray(self.spatial_shape, dtype=np.int64)), axis=1)

    def lookup(self, q: np.ndarray) -> np.ndarray:
        """Rows of the voxels at ``q (M, 3)`` (int64), ``-1`` where no active voxel is."""
        keys = self.keys_of(q)
        if len(self._sorted) == 0:
            return np.full(len(keys), -1, np.int64)
        pos = np.searchsorted(self._sorted, keys).clip(max=len(self._sorted) - 1)
        found = (self._sorted[pos] == keys) & (keys >= 0)
        return np.where(found, self._perm[pos], -1)

    def linear_index(self, order: str = "xyz") -> np.ndarray:
        """In-grid linear index of every voxel: ``(x * Y + y) * Z + z`` (``"xyz"``, spconv's for batch 0)."""
        if order != "xyz":
            raise ValueError("only the xyz order is defined")
        X, Y, Z = self.spatial_shape
        c = self.coords
        return (c[:, 0] * Y + c[:, 1]) * Z + c[:, 2]

    def describe(self) -> Dict[str, Any]:
        return {"num_active": self.num_active, "spatial_shape": list(self.spatial_shape),
                "out_of_grid": int((~self.in_grid()).sum()) if self.num_active else 0}


# ---------------------------------------------------------------------------------------------- rulebooks

def kernel_offsets(kernel: Any = 3, dilation: Any = 1) -> np.ndarray:
    """``(K, 3)`` int64 tap offsets ``(a * d0, b * d1, c * d2)`` in tap order ``k = (a * k1 + b) * k2 + c``."""
    k = _triple(kernel, "kernel")
    d = _triple(dilation, "dilation")
    a, b, c = np.meshgrid(np.arange(k[0]), np.arange(k[1]), np.arange(k[2]), indexing="ij")
    return np.stack([a.ravel() * d[0], b.ravel() * d[1], c.ravel() * d[2]], axis=1).astype(np.int64)


def conv_out_shape(spatial_shape: Any, kernel: Any, stride: Any, padding: Any, dilation: Any = 1
                   ) -> Tuple[int, int, int]:
    """spconv / Conv3d output grid: ``(in + 2 p - d (k - 1) - 1) // s + 1`` per axis."""
    s_in, k, s, p, d = (np.asarray(_triple(v, n)) for v, n in ((spatial_shape, "spatial_shape"), (kernel, "kernel"),
                                                                 (stride, "stride"), (padding, "padding"),
                                                                 (dilation, "dilation")))
    out = (s_in + 2 * p - d * (k - 1) - 1) // s + 1
    if np.any(out <= 0):
        raise ValueError(f"empty output grid {tuple(out)}")
    return tuple(int(v) for v in out)  # type: ignore[return-value]


def subm_neighbor_map(level: SparseLevel, kernel: Any = 3, dilation: Any = 1, *, rule: str = "spconv") -> np.ndarray:
    """Sub-manifold conv neighbour map ``(N, K)`` int32: ``nmap[o, k]`` = the input row read by tap ``k`` of output
    ``o`` (the level's own rows), ``-1`` if none (module docstring for ``rule``)."""
    if rule == "alias":
        raise NotImplementedError("the linear-key alias mode (S:ptv3:388-393) is added by the PTv3 port")
    if rule not in RULES:
        raise ValueError(f"rule must be one of {RULES}")
    k = _triple(kernel, "kernel")
    if any(v % 2 == 0 for v in k):
        raise ValueError(f"sub-manifold kernels are odd, got {k}")
    d = np.asarray(_triple(dilation, "dilation"), dtype=np.int64)
    pad = (np.asarray(k, dtype=np.int64) // 2) * d
    offs = kernel_offsets(k, d) - pad                                  # (K, 3) centred offsets
    K = len(offs)
    n = level.num_active
    out = np.full((n, K), -1, dtype=np.int32)
    if n == 0:
        return out
    c = level.coords
    q = (c[:, None, :] + offs[None, :, :]).reshape(-1, 3)
    rows = level.lookup(q).reshape(n, K)
    if rule == "spconv":
        centre = K // 2
        own_in = level.in_grid()                                      # (N,)
        nb_in = level.in_grid(q).reshape(n, K)
        low = np.arange(K) < centre
        accept = np.where(low[None, :], nb_in, own_in[:, None])
        rows = np.where(accept, rows, -1)
        rows[:, centre] = np.arange(n)                                # the centre tap is the voxel itself
    out[:] = rows
    return out


def strided_neighbor_map(level: SparseLevel, kernel: Any, stride: Any, padding: Any, dilation: Any = 1
                         ) -> Tuple[SparseLevel, np.ndarray]:
    """Regular sparse conv: ``(out_level, nmap (N_out, K) int32)``; ``nmap[o, k]`` = the input row with
    ``i = o * s - p + k * d`` (``-1``: none). Output voxels in ascending ``(x, y, z)`` linear-key order. The input
    voxels must lie inside their grid (out-of-grid inputs would change spconv's set; refused)."""
    k = _triple(kernel, "kernel")
    s = np.asarray(_triple(stride, "stride"), dtype=np.int64)
    p = np.asarray(_triple(padding, "padding"), dtype=np.int64)
    d = np.asarray(_triple(dilation, "dilation"), dtype=np.int64)
    out_shape = conv_out_shape(level.spatial_shape, k, s, p, d)
    if level.num_active and not level.in_grid().all():
        raise ValueError("strided rulebooks need every input voxel inside its grid")
    offs = kernel_offsets(k, d)                                         # (K, 3), uncentred: tap k at + k * d
    K = len(offs)
    c = level.coords
    num = c[:, None, :] + p - offs[None, :, :]                          # = o * s for a hit
    ok = np.all(num % s == 0, axis=2)
    o = num // s
    ok &= np.all((o >= 0) & (o < np.asarray(out_shape, dtype=np.int64)), axis=2)
    ii, kk = np.nonzero(ok)
    oc = o[ii, kk]
    X, Y, Z = out_shape
    okey = (oc[:, 0] * Y + oc[:, 1]) * Z + oc[:, 2]
    uniq, inv = np.unique(okey, return_inverse=True)
    out_coords = np.stack([uniq // (Y * Z), (uniq // Z) % Y, uniq % Z], axis=1).astype(np.int64)
    out_level = SparseLevel(out_coords, out_shape)
    nmap = np.full((len(uniq), K), -1, dtype=np.int32)
    nmap[inv.reshape(-1), kk] = ii                                      # one input per (output, tap)
    return out_level, nmap


def pairs_count(nmap: np.ndarray) -> int:
    """Number of (input, output, tap) pairs of a neighbour map (the rulebook size; FLOPs = 2 Cin Cout pairs)."""
    return int(np.count_nonzero(np.asarray(nmap) >= 0))


# ------------------------------------------------------------------------------------------- device forms

def gather_index(nmap: np.ndarray, *, sentinel: int, rows: Optional[int] = None) -> np.ndarray:
    """``(rows, K)`` uint32: ``nmap`` with ``-1`` -> ``sentinel`` (the zero row of the feature table) and the rows
    past ``len(nmap)`` (capacity padding) all ``sentinel``. ``sentinel`` must exceed every valid row."""
    nm = np.asarray(nmap)
    n, K = nm.shape
    rows = n if rows is None else int(rows)
    if rows < n:
        raise ValueError(f"{n} active rows exceed the capacity {rows}")
    if n and nm.max() >= sentinel:
        raise ValueError(f"row {int(nm.max())} >= sentinel {sentinel}")
    if sentinel < 0 or sentinel >= 2 ** 32:
        raise ValueError("sentinel must fit uint32")
    out = np.full((rows, K), sentinel, dtype=np.uint32)
    out[:n] = np.where(nm >= 0, nm, sentinel).astype(np.uint32)
    return out


def im2col_index(nmap: np.ndarray, *, sentinel: int, rows: Optional[int] = None, block: int = TILE_ROWS
                 ) -> np.ndarray:
    """The tile-ordered index of the one-gather im2col (C28, probe P4): ``(rows / block, K * block)`` uint32 with
    ``index[b, k * block + v] = gather_index[b * block + v, k]``. ``ttnn.embedding`` of it into a ``[1, 1, V, Cin]``
    table (``Cin % 32 == 0``) with ``layout=TILE`` yields ``[rows / block, K * block, Cin]``, whose tiles, in memory
    order, are those of the ``[rows, K * Cin]`` im2col matrix (row ``o``: the ``K`` tap rows of output ``o``
    concatenated). ``rows`` (default ``len(nmap)`` rounded up) must be a multiple of ``block``."""
    nm = np.asarray(nmap)
    n, K = nm.shape
    rows = -(-n // block) * block if rows is None else int(rows)
    if rows % block:
        raise ValueError(f"rows {rows} is not a multiple of the block {block}")
    g = gather_index(nm, sentinel=sentinel, rows=rows)                  # (rows, K)
    return np.ascontiguousarray(g.reshape(rows // block, block, K).transpose(0, 2, 1).reshape(rows // block,
                                                                                               K * block))


def im2col_numpy(table: np.ndarray, index: np.ndarray, *, kernel_volume: int, block: int = TILE_ROWS) -> np.ndarray:
    """Oracle of the device im2col: the ``[rows, K * C]`` matrix that :func:`im2col_index` describes, built from the
    row table ``(V, C)`` (``index`` values are rows of ``table``)."""
    t = np.asarray(table)
    idx = np.asarray(index, dtype=np.int64)
    nb = idx.shape[0]
    K = int(kernel_volume)
    if idx.shape[1] != K * block:
        raise ValueError(f"index has {idx.shape[1]} columns, expected {K} x {block}")
    g = idx.reshape(nb, K, block).transpose(0, 2, 1).reshape(nb * block, K)     # back to (rows, K)
    return t[g].reshape(nb * block, K * t.shape[1])


def dense_gather_index(level: SparseLevel, *, sentinel: int, split_axis: int = 2) -> np.ndarray:
    """To-dense as gathers: ``(S, A * B)`` uint32 with ``index[s, a * B + b]`` = the row of voxel at ``(a, b)`` in
    slice ``s`` of ``split_axis`` (the two other axes in (x, y, z) order give ``a``, ``b``), ``sentinel`` where the
    cell is empty. BEVFusion's to-dense: ``split_axis = 2`` (z), ``index[z, x * Y + y]``; the slices are concatenated
    or interleaved on the channel axis afterwards."""
    if split_axis not in (0, 1, 2):
        raise ValueError("split_axis must be 0, 1 or 2")
    if level.num_active and not level.in_grid().all():
        raise ValueError("to-dense needs every voxel inside the grid")
    if level.num_active and level.num_active > sentinel:
        raise ValueError("sentinel must not be a valid row")
    axes = [a for a in range(3) if a != split_axis]
    S = level.spatial_shape[split_axis]
    A, B = level.spatial_shape[axes[0]], level.spatial_shape[axes[1]]
    out = np.full((S, A * B), sentinel, dtype=np.uint32)
    c = level.coords
    out[c[:, split_axis], c[:, axes[0]] * B + c[:, axes[1]]] = np.arange(level.num_active, dtype=np.uint32)
    return out


def dense_numpy(features: np.ndarray, level: SparseLevel) -> np.ndarray:
    """``(X, Y, Z, C)`` dense tensor of the level's features (zeros elsewhere): ONNX ``ScatterND`` into
    ``[X, Y, Z, C]``, the reference of the to-dense gathers."""
    f = np.asarray(features)
    X, Y, Z = level.spatial_shape
    out = np.zeros((X, Y, Z, f.shape[1]), dtype=f.dtype)
    c = level.coords
    out[c[:, 0], c[:, 1], c[:, 2]] = f[: level.num_active]
    return out


# ------------------------------------------------------------------------------------------------ capacity

def select_capacity(count: int, buckets: Sequence[int]) -> Tuple[int, bool]:
    """The smallest bucket ``>= count`` and ``False``; the largest bucket and ``True`` (overflow: the caller keeps
    that many and flags the frame) when none holds it. Buckets are grid-independent constants (PLAN.md 0.2)."""
    b = sorted(int(v) for v in buckets)
    if not b or b[0] <= 0:
        raise ValueError("buckets must be positive")
    for v in b:
        if count <= v:
            return v, False
    return b[-1], True


# ------------------------------------------------------------------------------------------------- oracles

def sparse_conv_numpy(features: np.ndarray, nmap: np.ndarray, weight: np.ndarray, bias: Optional[np.ndarray] = None,
                      *, dtype: Any = np.float64) -> np.ndarray:
    """The gather form of the sparse conv: ``out[o] = sum_k W[:, k, :] @ features[nmap[o, k]] (+ bias)``.
    ``weight`` KRSC ``[Cout, k0, k1, k2, Cin]``; accumulation in ``dtype`` (float64 default)."""
    f = np.asarray(features, dtype=dtype)
    w = np.asarray(weight, dtype=dtype)
    cout, cin = w.shape[0], w.shape[-1]
    wk = w.reshape(cout, -1, cin)                                       # (Cout, K, Cin)
    nm = np.asarray(nmap)
    out = np.zeros((len(nm), cout), dtype=dtype)
    for k in range(nm.shape[1]):
        sel = nm[:, k] >= 0
        if sel.any():
            out[sel] += f[nm[sel, k]] @ wk[:, k, :].T
    if bias is not None:
        out += np.asarray(bias, dtype=dtype)
    return out


def dense_conv3d_numpy(features: np.ndarray, level: SparseLevel, weight: np.ndarray, out_coords: np.ndarray, *,
                       stride: Any = 1, padding: Any = 0, dilation: Any = 1) -> np.ndarray:
    """Brute-force dense 3-D cross-correlation (``torch.nn.Conv3d`` semantics, zero padding) of the densified input,
    read at ``out_coords``: the definition a sparse conv must equal on its active outputs. Float64; small grids."""
    w = np.asarray(weight, dtype=np.float64)
    k = w.shape[1:4]
    s = np.asarray(_triple(stride, "stride"))
    p = np.asarray(_triple(padding, "padding"))
    d = np.asarray(_triple(dilation, "dilation"))
    X, Y, Z = level.spatial_shape
    dense = dense_numpy(np.asarray(features, dtype=np.float64), level)
    oc = np.asarray(out_coords, dtype=np.int64)
    out = np.zeros((len(oc), w.shape[0]))
    for a in range(k[0]):
        for b in range(k[1]):
            for c in range(k[2]):
                i = oc * s - p + np.array([a, b, c]) * d
                inside = np.all((i >= 0) & (i < np.array([X, Y, Z])), axis=1)
                if inside.any():
                    ii = i[inside]
                    out[inside] += dense[ii[:, 0], ii[:, 1], ii[:, 2]] @ w[:, a, b, c, :].T
    return out


def neighbor_stats(nmap: np.ndarray) -> Dict[str, Any]:
    """``{rows, pairs, mean_taps, max_taps}`` of a neighbour map (diagnostics, PORT_LOG tables)."""
    nm = np.asarray(nmap)
    taps = (nm >= 0).sum(axis=1) if len(nm) else np.zeros(0)
    return {"rows": int(len(nm)), "pairs": int(taps.sum()), "mean_taps": float(taps.mean()) if len(nm) else 0.0,
            "max_taps": int(taps.max()) if len(nm) else 0}


def iter_taps(kernel: Any) -> Iterable[Tuple[int, Tuple[int, int, int]]]:
    """``(k, (a, b, c))`` in tap order."""
    k = _triple(kernel, "kernel")
    for a in range(k[0]):
        for b in range(k[1]):
            for c in range(k[2]):
                yield (a * k[1] + b) * k[2] + c, (a, b, c)