Majid Al-RaimiSparse inputs and sparse convolution

COE 592Lecture 4.2Part 11

Sparse inputs and sparse convolution

Point clouds are mostly empty space, so convolution should compute only where inputs exist: sparse convolution keeps output sparsity equal to input sparsity and is a sparse set of dense matrix multiplies driven by an input, output and weight map.

Concepts
3
Slides
84-96
Reading
18 min
Understood
0/3 concepts

Why this part matters

The sparsity in this part is not something you create by pruning or by passing activations through ReLU. It is handed to you by the physical world. A LiDAR scanner on a car returns a few hundred thousand points inside a volume of billions of voxels, and the winning entries on SemanticKITTI, nuScenes and Waymo all process those points with sparse convolution (Tang et al., 2022). If your research project puts 3D perception on an embedded board, this is the operator you will be optimizing.

The part has one story with three turns. First, why an ordinary convolution is the wrong tool for a point cloud: it dilates the empty space into a dense blur within a few layers. Second, the fix, which computes outputs only where inputs exist, and the small bookkeeping structure that describes exactly which multiplications remain. Third, how a GPU actually runs that bookkeeping as a handful of dense matrix multiplies, which is where parts 12 and 13 pick up with TorchSparse and PointAcc. Every idea rests on one list of tuples, so by the end you should be able to write that list for a small grid by hand.

By the end you can

  1. Explain the submanifold dilation problem with the 1, then 3^d, then 5^d rule and the 17-of-20 example from the slides.
  2. Define submanifold sparse convolution and state why the output point set equals the input point set.
  3. Write the update rule f_out = f_out + f_in x W_(dx,dy) and build the (In, Out, Wgt) map for a small grid by hand.
  4. Group a map by weight offset and cost the resulting gather, matmul, scatter pipeline in MACs.
  5. Distinguish point-cloud sparsity from ReLU activation sparsity and from pruned weight sparsity.

Start with a number. Voxelize an outdoor LiDAR frame into a regular 3D grid and count how many cells hold at least one point. PointAcc (Lin et al., MICRO 2021) reports that outdoor point clouds usually have a density below 0.01 percent, while a conventional CNN takes in a 100 percent dense image. That is the ~0.01% written above the sparse column of the slide. Indoor scans and single objects sit below 1 percent. Even after ReLU, an ImageNet network still keeps about half of its activations nonzero, so an image is up to four orders of magnitude denser than a LiDAR scan.

How dense the input really is (Lin et al., 2021)

ImageNet image at the input
100 percent of pixels carry a value
Same network after ReLU
about 50 percent nonzero on average
Indoor scene or single object, voxelized
below 1 percent of voxels occupied
Outdoor LiDAR frame, voxelized
below 0.01 percent of voxels occupied

The roadmap places this under activation sparsity, next to EIE and the M:N tensor cores, and that is correct as far as it goes: the zeros are in the data, not the weights. But the source is different in a way that changes everything downstream. ReLU zeros and pruned weight zeros land wherever the arithmetic puts them. Point-cloud zeros are the empty air between surfaces. PointAcc calls this sparsity "fundamentally different" because "the sparsity pattern is constrained by the physical objects in the real world", and draws the consequence that matters here: the nonzero points should never dilate during computation. In the vocabulary of part 07 it is still dynamic, since every frame has a new pattern, but it is a pattern with geometric meaning.

What a dense convolution does to empty space

Now apply an ordinary 3 x 3 convolution to a grid with a single active cell. The kernel window touches that cell from nine different positions, so nine outputs become nonzero. Apply a second layer and the nine become twenty-five. Graham, Engelcke and van der Maaten (CVPR 2018) state the rule in d dimensions: a single active site becomes 3^d active sites after one convolution and 5^d after two. In 3D that is 1, then 27, then 125. They name it the submanifold dilation problem, because a surface (a 2D submanifold in 3D space) thickens into a slab and the sparsity that made the input cheap is gone within a few layers.

PL=(2L+1)dfor one active site after L dense 3d convolutions\lvert \mathcal{P}_L \rvert = (2L + 1)^d \quad \text{for one active site after } L \text{ dense } 3^d \text{ convolutions}
Dilation of a single active site (Graham et al., 2018)

The slide shows the same effect on a small grid you can check by eye. The input is 4 rows by 5 columns with four active cells at rows and columns (1, 1), (2, 2), (2, 4) and (3, 3). Each active cell activates every output its 3 x 3 window touches, clipped to the grid: nine for the first two points, six each for the two that sit against an edge. The union of those windows is 17 of 20 cells, exactly the orange region drawn on the conventional side. Only (0, 3), (0, 4) and (3, 0) stay empty, and a second layer would fill those too.

Same four inputs, same 3 by 3 kernel. Dense convolution lights every cell a window touches; submanifold convolution lights only the four input positions.
StageActive cellsDensityWhy
Input4 of 2020 percentThe four teal cells
After one dense 3 by 3 layer17 of 2085 percentUnion of the four 3 by 3 windows, clipped to the grid
After one submanifold 3 by 3 layer4 of 2020 percentOnly the four input positions
After a second dense layer20 of 20100 percentEvery cell now sits within one step of an active cell
Active cells on the slide grid, layer by layer

The fix: compute only where an input exists

Sparse convolution, in the submanifold form the deck uses, changes one rule. An output site is computed if and only if the input site at the same coordinate is active. Graham et al. define it exactly that way: the filter size is odd, the input is padded so the output keeps the input's size, and an output is active only when the central site of its receptive field is active. TorchSparse writes the same fact as a set equation, P_in = P_out, against the dense case where P_in is only a subset of P_out. On the slide grid the sparse side stays at four orange cells, and after a hundred layers it would still be four.

The pair of ring images at the bottom of the slide comes from two different figures of the Graham papers. The left ring is Figure 2 of the CVPR 2018 paper (Figure 1 of the 2017 arXiv version): a thin curve pushed through two ordinary 3 x 3 convolutions has smeared into a grey band. The right ring is Figure 3 of the CVPR 2018 paper: the submanifold receptive field centred on one active site, drawn in green, with the empty sites it ignores in red, so the ring stays a ring. That is the whole motivation in one picture: the operator must respect the geometry, or the geometry disappears.

Why a whole part on one operator? TorchSparse's introduction lists the stakes. All top five segmentation submissions on SemanticKITTI and nine of the top ten on nuScenes are built on sparse convolution, yet the operator is not supported by TensorRT or TVM, and the best library of the day ran MinkowskiNet at 8 FPS on a GTX 1080Ti, a desktop GPU far above anything on an embedded board. The gap between what the algorithm avoids and what the hardware delivers is the research territory of parts 12 and 13.

Recall

Why is the output of a submanifold sparse convolution exactly as sparse as its input, and what happens to one active site under two dense 3 by 3 layers instead?

An output is computed only where an input point exists (output active if and only if the central input site is active), so P_out = P_in. Under dense convolution one active site becomes 3^d after one layer and 5^d after two: in 2D, 1, then 9, then 25.

Quick check

A 3 by 3 submanifold sparse convolution runs over four active cells of a 4 by 5 grid. How many output cells are nonzero?

Follow the boxed point P0 at row 1, column 1 through a 3 x 3 convolution. A 3 x 3 kernel is not one weight; it is nine weight matrices, one per tap, each of shape C_in x C_out. TorchSparse writes them as W_δ for each offset δ in {-1, 0, 1}^2. The deck names them W_(dx,dy), with dx the row offset and dy the column offset, and the visual and simulator below use the same labels. Every time P0 lands in an output's window, exactly one of those nine matrices multiplies its feature vector, and which one depends only on where the output sits relative to P0.

The deck's convention, consistent across all nine slides, is that the input P sits at the output Q shifted by the offset: P = Q + (dx, dy) in (row, column). The first entry on the slide makes it concrete. Output Q0 is the top-left corner of P0's window at (0, 0), so P0 = Q0 + (1, 1) and the weight is W_(1,1). The centre entry, where Q4 is P0's own position, uses W_(0,0). The bottom-right corner at (2, 2) uses W_(-1,-1).

The map is the computation

Each such relation is one tuple, and the list of all of them is what the slides call the maps, written (In, Out, Wgt). TorchSparse defines the map for a layer as the set M = {(p_j, q_k, W_δ)} and describes the whole layer as one loop: iterate over the map and, for each entry, accumulate the input feature vector times the entry's weight matrix into the output feature vector. That is the update rule printed at the bottom-left of every slide in this run.

fout(Q)fout(Q)+fin(P)W(dx,dy)for every entry (P,Q,W(dx,dy)) in the mapf_{\text{out}}(Q) \leftarrow f_{\text{out}}(Q) + f_{\text{in}}(P)\, W_{(dx,dy)} \quad \text{for every entry } (P, Q, W_{(dx,dy)}) \text{ in the map}
The update rule: one vector-matrix product per map entry
xkout=δΔD(K)j1[pj=sqk+δ](xjinWδ)x^{\text{out}}_k = \sum_{\delta \in \Delta^D(K)} \sum_{j} \mathbf{1}\left[p_j = s \cdot q_k + \delta\right]\,\left(x^{\text{in}}_j \cdot W_\delta\right)
The same rule as TorchSparse writes it (Eq. 1), with stride s and the indicator selecting matching pairs

Read the indicator as the map-building test. For each output q_k and each offset δ, look up the coordinate s·q_k + δ. If an input point p_j sits there, the triple (p_j, q_k, W_δ) joins the map; if not, nothing is added and nothing is computed. TorchSparse's Algorithm 1 is exactly this double loop with a hash-table lookup on coordinates. The heading of the slides now reads correctly: a sparse convolution is a sparse set of dense matrix multiply-accumulates, and the sparsity lives entirely in which tuples exist.

P0's window covers nine taps. Only two arcs survive: through W(0,0) to its own output and through W(-1,-1) to the point at (2, 2). The other seven taps point at empty cells.

Nine entries become two

Now compare the two columns of the slide for P0. The conventional column lists one entry per tap, nine in all, because every cell of P0's window is an output that a dense layer computes. The sparse column applies the submanifold rule from the previous concept: an output must itself be an input position. Inside P0's window only two cells qualify, P0's own position and (2, 2), where P1 sits. So the map keeps exactly two tuples and marks the other seven "No compute".

Conventional entryOutput positionConventionalSubmanifold sparse
(P0, Q0, W1,1)(0, 0)computedno compute
(P0, Q1, W1,0)(0, 1)computedno compute
(P0, Q2, W1,-1)(0, 2)computedno compute
(P0, Q3, W0,1)(1, 0)computedno compute
(P0, Q4, W0,0)(1, 1)computedkept as (P0, Q0, W0,0)
(P0, Q5, W0,-1)(1, 2)computedno compute
(P0, Q8, W-1,1)(2, 0)computedno compute
(P0, Q9, W-1,0)(2, 1)computedno compute
(P0, Q10, W-1,-1)(2, 2)computedkept as (P0, Q1, W-1,-1)
P0's nine map entries with P = Q + (dx, dy); output positions are (row, column)

Costing the whole grid, not just P0

The slide follows one point, so extend it to all four. Conventionally each point contributes one entry per in-grid tap: 9 for P0, 9 for the point at (2, 2), 6 for (2, 4) against the right edge and 6 for (3, 3) against the bottom, 30 in total. The submanifold map has one centre entry per point plus one entry for every ordered pair of points within one step of each other. Three such pairs exist, (1, 1) with (2, 2), (2, 2) with (3, 3) and (2, 4) with (3, 3), each counted in both directions, giving 4 + 6 = 10 entries. Each entry is a (1 x C_in)(C_in x C_out) product, so with C_in = C_out = 64 an entry costs 4,096 MACs.

MethodEntriesMACsWhat is counted
Fully dense 3 by 3 over 20 outputs20 x 9 = 180737,280Every output visits every tap, zeros included
Conventional, skipping zero inputs9 + 9 + 6 + 6 = 30122,880Each point feeds every in-grid output its window touches
Submanifold sparse4 + 3 x 2 = 1040,960Four centre entries plus three neighbouring pairs in both directions
One 3 by 3 layer on the 4 by 5 slide grid with C_in = C_out = 64

Two lessons hide in that table. Zero-skipping alone, the trick EIE relied on, already removes most of the work on this grid, but it still writes the dilated 17-cell output and so the next layer starts from 17 points instead of 4. The submanifold map is the only row whose cost stays proportional to the number of points layer after layer. On a real LiDAR frame with hundreds of thousands of points in a grid of billions of voxels, the fully dense row is not merely slow; it does not fit in memory.

SimulatorSparse convolution map builder
Input points, 6 by 6 grid
Kernel offset (dx, dy), rows per weight
Map for W-1,-1(P0, Q1, W-1,-1) f1 = f1 + f0 × W-1,-1(P3, Q4, W-1,-1) f4 = f4 + f3 × W-1,-1
Input points5of 36teal cells, P = Q + (dx, dy)
Dense output cells27of 36union of the 3 by 3 windows
Conventional entries45entriesone per input times each in-grid tap
Submanifold entries11entriesoutput must be an input position
Matmuls launched5of 9one per non-empty offset
Fully dense MACs1.33 MMAC36 outputs × 9 taps × C²
Conventional MACs184.3 kMACzero-skipping, still dilates
Submanifold MACs45.1 kMACentries × C² with C = 64

Symmetry check: W-1,-1 has 2 entries and W1,1 has 2.

Click cells to place or remove points, then pick a kernel offset. The list shows every map entry for that weight using the deck's convention: input P sits at output Q shifted by the row and column offset (dx, dy), and the output must itself be an input position. The counts in the offset picker are the row counts of the gathered matrices on slide 96, so the number of non-empty offsets is the number of separate matmuls the existing GPU implementation launches. Compare the three MAC readouts: fully dense visits every cell, conventional zero-skipping still fills the dilated set, and submanifold pays only for entries whose output is a real point.

Recall

Write the sparse convolution update rule and name every symbol in it.

f_out(Q) = f_out(Q) + f_in(P) x W_(dx,dy) for each map entry (P, Q, W_(dx,dy)). P is an input point, Q is an output point at P - (dx, dy), and W_(dx,dy) is the C_in x C_out weight matrix for that kernel tap.

Recall

On slide 95, how many map entries does P0 have conventionally and sparsely, and why?

Nine versus two. A dense layer computes all nine cells of P0's 3 x 3 window, but only two of those cells are input positions: (1, 1) itself and (2, 2), where P1 sits. The other seven outputs do not exist, so their entries are never built.

Quick check

In the entry (P0, Q1, W-1,-1) with the deck's convention P = Q + (dx, dy), where is Q1 relative to P0?

Quick check

Which statement about submanifold sparse convolution is correct?

Slide 95 closes the P0 story with a count, nine matrix multiplications against two, and slide 96 asks the practical question: how does a GPU run a list of tuples? Take its workload, a 5 x 5 grid with five points P0 at (1, 1), P1 at (2, 2), P2 at (2, 4), P3 at (3, 2) and P4 at (4, 3). Its map has eleven entries, and the box on the slide lists them not in point order but sorted by weight offset. That ordering is the whole idea.

Running the update rule one entry at a time means eleven vector-matrix products, each a single row of C_in values against a C_in x C_out matrix. TorchSparse notes that the utilization of matrix-vector multiplication is rather low on a GPU, whose tensor cores want tall matrices on both sides. The escape is to notice that entries sharing the same offset share the same matrix. Stack their input rows into one contiguous matrix, multiply it by that one W_δ, and you have turned several vector-matrix products into one matrix-matrix product. Grouping by output or by input would not work: entries with the same output use different weights, and so do entries with the same input.

WeightEntries (In, Out)Rows gatheredNote
W-1,-1(P0, Q1), (P3, Q4)2highlighted on the slide
W-1,0(P1, Q3)1mirror of W1,0
W0,0(P0, Q0), (P1, Q1), (P2, Q2), (P3, Q3), (P4, Q4)5one entry per point, no data movement
W1,0(P3, Q1)1mirror of W-1,0
W1,1(P1, Q0), (P4, Q3)2mirror of W-1,-1
W-1,1, W0,1, W0,-1, W1,-1none0no kernel launch at all
Slide 96's eleven entries grouped by weight offset

Five of the nine offsets have entries, so this layer launches five matmuls with 2, 1, 5, 1 and 2 rows. The other four offsets have empty maps and launch nothing. Notice too that P2 at (2, 4) has no neighbour within one step, so it appears only in the W_(0,0) group: an isolated point costs exactly one entry.

Gather, matmul, scatter

The pipeline on slide 96 is the gather, matmul, scatter flow that every existing GPU implementation follows, written out for the highlighted offset W_(-1,-1). Its two entries are (P0, Q1) and (P3, Q4). Gather copies rows P0 and P3 out of the 5 x C_in input feature matrix into a 2 x C_in buffer. Matmul multiplies that buffer by the C_in x C_out weight to produce a 2 x C_out partial sum. Scatter adds row one into output Q1 and row two into Q4, which is the pair of equations under the figure: f1 = f1 + f0 x W_(-1,-1) and f4 = f4 + f3 x W_(-1,-1). Then the next offset takes its turn with its own rows.

For W(-1,-1): rows P0 and P3 are gathered into a two-row buffer, multiplied by the one weight matrix, and the two partial sums are scatter-added into Q1 and Q4. Every other row stays untouched.

Worked example

Costing the W(-1,-1) group with C_in = C_out = 64

  1. Gather

    Read rows P0 and P3 of the 5 x 64 input matrix into a 2 x 64 buffer: 128 values moved, no arithmetic.
  2. Matmul

    Multiply the 2 x 64 buffer by the 64 x 64 matrix W_(-1,-1): 2 x 64 x 64 = 8,192 MACs, producing a 2 x 64 partial sum.
  3. Scatter-add

    Add partial-sum row one into f1 and row two into f4: 128 additions into the 5 x 64 output matrix.
  4. Repeat for the other four non-empty offsets

    Rows gathered are 1, 5, 1 and 2, so the layer does 11 row-matrix products in five kernel launches.
  5. Layer cost

    11 x 4,096 = 45,056 MACs, against 159,744 for a zero-skipping conventional convolution on the same five points and 921,600 for a fully dense 3 x 3 over all 25 cells.
MethodEntriesMACs
Fully dense 3 by 3 over 25 outputs25 x 9 = 225921,600
Conventional, skipping zero inputs9 + 9 + 6 + 9 + 6 = 39159,744
Submanifold sparse, 5 matmuls2 + 1 + 5 + 1 + 2 = 1145,056
Slide 96 workload, one layer, C_in = C_out = 64

The slide's heading calls this weight-stationary: the weight matrix for one offset stays put while the gathered rows stream through it, and a separate matmul runs for each different weight. TorchSparse's Algorithm 2 is literally a loop over offsets that performs gather, matmul and scatter for each. The rule book in Graham et al. is the same design described from the sparse-convolution side: for each row (j, k) in the rule for offset i, multiply row j of the input matrix by W^i and add it to row k of the output, which they note runs efficiently on a GPU precisely because it is a matrix-matrix multiply-add. SECOND (Yan et al., 2018) moved the rule generation itself from a CPU hash table to the GPU, since the CPU version was slow and required data transfer between CPU and GPU on every layer.

A symmetry you can check on the slide

Look at the row counts again: W_(-1,-1) and W_(1,1) both have two rows, W_(-1,0) and W_(1,0) both have one. That is not luck. If (P, Q, W_δ) is an entry, then P and Q are both points and Q = P + (-δ), so (Q, P, W_(-δ)) is also an entry. TorchSparse section 4.2.1 proves this one-to-one correspondence for any odd kernel at stride 1, and the (0, 0) map is special for a second reason: every point maps to itself, so it needs no gather and no scatter at all. Part 12 builds its first optimization on exactly this pairing, and the simulator above reports the check on every grid you draw.

Recall

Name the three stages of the existing GPU implementation and explain why entries are grouped by weight offset.

Gather the input rows named by the map for one W_δ into a contiguous matrix, run one matrix-matrix multiply against W_δ, then scatter-add the partial sums into the listed output rows. Grouping by weight is the only grouping that lets several entries share one matmul, and matrix-vector products alone underuse the GPU.

Recall

From the slide 96 list, how many matmuls does the layer launch and with how many rows each?

Five: offsets (-1,-1), (-1,0), (0,0), (1,0) and (1,1) with 2, 1, 5, 1 and 2 rows. The other four offsets have empty maps and launch nothing.

Quick check

Why does the GPU implementation group map entries by weight offset before multiplying?

  • Entries: for stride 1 with an odd kernel, count(δ) = count(-δ), and the (0, 0) map has exactly one entry per point.
  • Matmuls per layer: the number of offsets whose map is non-empty, at most K^D.
  • MACs per layer: total entries times C_in x C_out, independent of how the entries are grouped. Grouping changes utilization and data movement, never the arithmetic count.

Recap

If you remember nothing else

  • Outdoor LiDAR voxel grids are below 0.01 percent dense; ImageNet inputs are 100 percent dense and about 50 percent after ReLU.
  • Dense convolution dilates: one active site becomes 3^d, then 5^d; on the slide grid 4 of 20 cells become 17 of 20 after one 3 by 3 layer.
  • Submanifold sparse convolution computes outputs only at input positions, so P_out = P_in and nothing dilates.
  • The computation is a map of (In, Out, Wgt) entries with P = Q + (dx, dy); each entry does f_out = f_out + f_in x W_(dx,dy).
  • For P0 on slide 95: 9 conventional entries, 2 sparse entries. Whole 4 by 5 grid: 30 versus 10.
  • Entries sharing W_(dx,dy) are gathered into one matrix, multiplied once, then scatter-added (weight-stationary). Slide 96: 11 entries, five matmuls with 2, 1, 5, 1, 2 rows.
  • At stride 1 with an odd kernel, the maps for an offset and its mirror always have equal size.
  • Per-offset matmuls are irregular and gather plus scatter can take up to 50 percent of runtime: the problems TorchSparse and PointAcc attack in parts 12 and 13.

Sources