Majid Al-RaimiPruning granularity: from irregular to regular

COE 592Lecture 4.1Part 03

Pruning granularity: from irregular to regular

Fine-grained versus coarse-grained pruning on a 2D weight matrix, the four dimensions of a convolution weight tensor, and the five commonly used granularities from fine-grained to channel-level.

Concepts
3
Slides
14-18
Reading
18 min
Understood
0/3 concepts

Why this part matters

Sooner or later your embedded project will hit this moment: a network that is 90% zeros runs no faster on the target MCU or GPU than the dense one did. Nothing is wrong with the pruning. What is wrong is the shape of the holes, and that shape is what this part is about.

Part 02 showed that pruning can remove most of a network's weights without hurting accuracy. The next question on the lecture's outline is in what pattern to remove them. We start with a single 8 x 8 matrix and two ways of cutting it, then open up a convolution weight to see that it has four dimensions and therefore many more ways to be cut, and finish with a ladder of five granularities adapted from Mao et al., who studied four. Exams ask you to order that ladder, to count the weights a given unit removes and to argue the trade between flexibility and acceleration. The same ladder returns in the next part as NVIDIA 2:4 sparsity, channel pruning and AMC, and again in lecture 04-2.

By the end you can

  1. Define pruning granularity and place fine-grained and coarse-grained pruning at its two ends.
  2. Explain why coarse-grained choices are a subset of fine-grained ones and what that does to attainable sparsity at equal accuracy.
  3. Read a convolution weight as [c_o, c_i, k_h, k_w] and count the weights in a filter, an input channel, a kernel and a row.
  4. Order fine-grained, pattern-based, vector-level, kernel-level and channel-level from irregular to regular and say what hardware each maps onto.
  5. Compute the sparsity and the surviving shape after removing a given unit.

Take a weight matrix with eight rows and eight columns, 64 weights in all, and decide to remove 24 of them. One way is to cross out rows 3, 4 and 7 completely. Another is to cross out 24 individual entries wherever the magnitude criterion of part 05 says they matter least. Both leave 40 weights and both reach a Sparsity of 24 / 64 = 37.5%. Everything else about them is different.

The unit you delete is the Pruning granularity. Deleting single weights is fine-grained, or unstructured, pruning. Deleting whole rows, columns, kernels or channels is coarse-grained, or structured, pruning. Two consequences pull in opposite directions as the unit grows. The number of masks you may choose from falls, which costs accuracy at a given Pruning ratio. The regularity of what survives rises, which is what lets ordinary hardware run the pruned layer faster.

Removing rows 3, 4 and 7 of an 8 x 8 matrix leaves a dense 5 x 8 block. Nothing needs an index, so any matrix library multiplies it as is.
Removing 24 scattered entries reaches the same 37.5% sparsity, but the matrix keeps its 8 x 8 footprint and each survivor needs a stored position.

Counting the choices

The slide calls fine-grained pruning "more flexible" and coarse-grained pruning "a subset of the fine-grained case". Both statements can be made exact by counting masks. A mask is the set of positions you zero, and the pruning problem of part 01 searches over masks for the one with the lowest loss.

Worked example

How many masks remove 24 of 64 weights

  1. Fine-grained

    Any 24 of the 64 positions may go, so there are C(64, 24) = 250,649,105,469,666,120 masks, about 2.5 x 10^17.
  2. Row-structured

    Any 3 of the 8 rows may go, so there are C(8, 3) = 56 masks.
  3. Nest one inside the other

    Every one of the 56 row masks is also a set of 24 individual positions, so it is one of the 2.5 x 10^17 fine-grained masks. The structured menu sits strictly inside the unstructured menu.
  4. Consequence for accuracy

    The best row mask can at most tie the best fine-grained mask on the loss. It can never beat it. At a fixed accuracy target, structured pruning therefore reaches equal or lower sparsity, never higher, for the best achievable mask under the same retraining.

Same sparsity, different search spaces

Weights removed
24 of 64 (37.5%)
Fine-grained masks
C(64, 24) ≈ 2.5 x 10^17
Row-structured masks
C(8, 3) = 56
Surviving shape, fine-grained
8 x 8 with 24 holes
Surviving shape, row-structured
5 x 8 dense

Why irregular means hard to accelerate

Multiply a vector by the 5 x 8 survivor and nothing special happens. It is just a smaller dense matrix, and every BLAS routine, every tensor core and every microcontroller loop handles it at full speed. That is the slide's "easy to accelerate (just a smaller matrix!)". Now multiply by the 8 x 8 matrix with 24 holes. A dense routine still performs all 64 multiplies, 24 of them by zero, and gains nothing. To gain, you must store the 40 surviving values together with their positions, in a compressed sparse format, and let the hardware skip the zeros one by one. Mao et al. draw the same line: fine-grained sparsity scatters the tensor into isolated weights and needs custom accelerators such as EIE or SCNN to exploit it, while filter and channel sparsity is simple to accelerate on general-purpose processors because it "is equivalent to obtaining a smaller dense model" (Mao et al., 2017, section 2).

The gap is measurable. Wen et al. trained AlexNet with structured sparsity and reported average layer-wise speedups of the convolutional layers of 5.1x on CPU and 3.1x on GPU with off-the-shelf libraries, against 3.0x and 0.9x for non-structured l1 sparsity (Wen et al., 2016, Table 4). The fine-grained model had more zeros. The structured model ran faster.

What the accuracy price looks like

Because coarse units search a smaller space, they lose a little accuracy at the same density (the fraction of weights kept). Mao et al. measured this on ImageNet with the same pipeline at every granularity. The differences are small between fine and vector grains and grow as the grain reaches whole kernels; pruning entire filters "loses nearly 1% validation accuracy at the very first pruning stage" on AlexNet (Mao et al., 2017).

NetworkDensity keptFine-grainedVector-levelKernel-level
AlexNet24.8%80.41%79.94%79.20%
VGG-1623.5%90.56%90.48%89.70%
ResNet-5040.0%92.34%92.26%92.07%
Top-5 ImageNet accuracy at equal density (Mao et al., 2017, Table 1)
GranularityDensity keptStorage relative to dense
Fine-grained22.1%33.0%
Vector-level29.9%34.5%
Kernel-level37.8%39.7%
AlexNet at baseline accuracy: density versus storage with 4-bit indices (Mao et al., 2017, Table 2)

Recall

Rows 3, 4 and 7 of an 8 x 8 weight matrix are pruned. What is the sparsity, and what shape survives?

24 of 64 weights become zero, a sparsity of 37.5%. The survivor is a dense 5 x 8 matrix with no index storage.

Recall

Why does the slide call coarse-grained choices a subset of the fine-grained case, and what does that imply for compression?

Every set of whole rows is also a set of individual weights, so each structured mask is one of the unstructured masks. The structured optimum can only tie the unstructured optimum, so at equal accuracy the attainable sparsity is lower or equal, never higher.

Quick check

Why is fine-grained pruning hard to accelerate on an ordinary GPU?

Quick check

Which statement about coarse-grained pruning matches slide 15?

A linear layer's weight is a matrix, so its only structured units are rows and columns. A convolution weight is richer. Slide 17 draws one: three rows of kernels stacked vertically, two columns of kernels side by side, and each kernel a 3 x 3 grid. Count the cells and you get 3 x 2 x 3 x 3 = 54 weights.

Those four factors are the four dimensions of a convolution weight. The tensor has shape [c_o, c_i, k_h, k_w]: c_o output channels, also called filters; c_i input channels; and a kernel of height k_h and width k_w. Goodfellow, Bengio and Courville write the same object as a 4-D kernel tensor K whose element K[i, j, k, l] is the connection strength between output channel i and input channel j at a row offset of k and a column offset of l (Goodfellow et al., 2016, section 9.5). PyTorch stores Conv2d.weight with exactly the slide's ordering, (out_channels, in_channels / groups, kernel_size[0], kernel_size[1]) (PyTorch documentation).

N=cocikhkwN = c_o \cdot c_i \cdot k_h \cdot k_w
Weights in one convolution layer
The slide 17 tensor with its four dimensions labeled on one object: three filters down, two input channels across, nine weights per kernel, 54 in all.

Slicing the tensor into units

The reason the lecture stops to name the dimensions is that each way of slicing the tensor is a candidate pruning unit. Mao et al. name the slices with array notation, and reading them that way makes the counts automatic. Fix the output channel and you have a filter, a 3-D slab. Fix the output and input channel and you have one kernel, a 2-D grid. Fix a row inside that kernel and you have a 1-D vector. Fix everything and you have a single scalar weight (Mao et al., 2017, section 3.2).

UnitSliceWeightsShare of 54
Single weightW[o, i, r, c]11.9%
Row inside a kernelW[o, i, r, :]k_w = 35.6%
KernelW[o, i, :, :]k_h x k_w = 916.7%
Filter (output channel)W[o, :, :, :]c_i x k_h x k_w = 1833.3%
Input channelW[:, i, :, :]c_o x k_h x k_w = 2750.0%
Units inside the 54-weight tensor of slide 17

Two formulas do all the work. A filter holds c_i · k_h · k_w weights, because it has one kernel per input channel. An input channel holds c_o · k_h · k_w weights, because every filter has a kernel that reads it. In the slide these are 18 and 27, and it is easy to mix them up, so picture the grid: a filter is a row of kernels, an input channel is a column of kernels.

The same counts at a realistic scale

Slide 17 is a toy so the picture fits. A VGG-style layer with 64 input and 64 output channels and 3 x 3 kernels has 64 x 64 x 9 = 36,864 weights, and the same slicing rules apply.

Unit removedWeights removedShareSurviving shape
One kernel90.024%Still [64, 64, 3, 3] with holes
One filter64 x 9 = 5761.56%[63, 64, 3, 3]
One input channel64 x 9 = 5761.56%[64, 63, 3, 3]
Sixteen input channels16 x 576 = 9,21625%[64, 48, 3, 3]
Removing units from a [64, 64, 3, 3] layer

Removing a whole filter has a second effect that Li et al. spell out. The filter's output feature map disappears, so the next layer no longer receives that channel, and the corresponding kernels in the next layer's weight tensor disappear too. Pruning m of the n filters of a layer removes m / n of the compute in that layer and in the one after it (Li et al., 2017).

Recall

Name the four dimensions of a convolution weight in the slide's order, and give the weight count for c_o = 3, c_i = 2 and 3 x 3 kernels.

[c_o, c_i, k_h, k_w]: output channels (filters), input channels, kernel height, kernel width. The count is 3 x 2 x 3 x 3 = 54.

Recall

How many weights go when you remove one kernel, one input channel and one filter from the 54-weight tensor?

One kernel removes k_h x k_w = 9 (16.7%). One input channel removes c_o x k_h x k_w = 27 (50%). One filter removes c_i x k_h x k_w = 18 (33.3%).

Quick check

A convolution layer has c_o = 3, c_i = 2 and 3 x 3 kernels. Channel-level pruning removes one input channel. How many weights are removed?

Put the two previous ideas together and the lecture's central figure appears. Slide 18 takes the same 54-weight tensor and prunes it five times, once per unit, arranging the results on an axis from irregular to regular. Read from left to right, the holes grow from single cells, to fixed shapes inside kernels, to whole rows, to whole kernels, to an entire column of kernels.

Five prunings of one tensor. Each step to the right deletes a larger unit, until the channel-level panel simply drops a column and leaves a dense 3 x 1 x 3 x 3 tensor.

Every panel can be counted, and counting them shows something the picture hides: every panel removes between a third and two thirds of the weights, so the real difference between them is not how much Sparsity they reach but how the zeros are arranged. The fine-grained panel keeps 23 scattered weights. The pattern-based panel keeps exactly four cells in every kernel, and each kept shape is a Tetris piece (a T or an L of four cells). The vector-level panel keeps seven of the eighteen kernel rows. The kernel-level panel drops the first filter's right kernel and the second filter's left kernel. The channel-level panel drops the whole second input channel across all three filters, and what survives is a dense [3, 1, 3, 3] tensor.

PanelWhat was removedWeights removedSparsitySurvivor
Fine-grained31 single weights3157.4%54 slots with holes
Pattern-based5 of 9 cells in each of 6 kernels6 x 5 = 3055.6%54 slots, one shape per kernel
Vector-level11 of 18 kernel rows11 x 3 = 3361.1%54 slots with empty rows
Kernel-level2 of 6 kernels2 x 9 = 1833.3%54 slots with empty kernels
Channel-level1 of 2 input channels3 x 9 = 2750.0%dense [3, 1, 3, 3]
Counting the five panels of slide 18 (54 weights each)

The ladder, rung by rung

Mao et al. organise four grains (fine, vector, kernel, filter) by how many dimensions each unit spans. The slide keeps their order, inserts pattern-based pruning between fine-grained and vector-level, and shows the 3-D rung as an input channel instead of Mao's filter (Mao et al., 2017, section 3.2). At the bottom, fine-grained pruning removes 0-D scalars anywhere. Pattern-based pruning keeps a small catalogue of fixed shapes inside each kernel, which the slide calls "like Tetris": the hardware knows the finite set of masks in advance instead of facing arbitrary holes. Niu et al. describe it as "fine-grained pruning patterns inside the coarse-grained structures" and, by compiling specialised code per pattern, ran networks on mobile CPUs and GPUs up to 44.5x faster than TensorFlow Lite and up to 11.4x faster than TVM with no accuracy loss (Niu et al., 2020). Vector-level pruning removes 1-D rows, W[o, i, r, :], of a kernel. Kernel-level pruning removes 2-D kernels, W[o, i, :, :]. At the top, channel pruning removes a 3-D slab: on the slide, an input channel W[:, i, :, :] across every filter.

GranularityUnitWeights per unitIndex costRuns efficiently on
Fine-grainedAny single weight (0-D)1One index per surviving weightCustom sparse engines such as EIE or SCNN
Pattern-basedA fixed mask inside each kernel5 per kernelOne pattern id per kernelCompilers that specialize code per pattern (PatDNN)
Vector-levelA row of a kernel (1-D)k_w = 3One index per surviving rowCustom 1-D convolution hardware such as Eyeriss
Kernel-levelA whole k_h x k_w kernel (2-D)9One index per surviving kernelCustom 2-D convolution hardware (not stock dense libraries)
Channel-levelAn input channel across all filters (3-D)27None: the tensor shrinksAny dense CPU or GPU library
Five granularities on the 54-weight tensor and the hardware each maps onto (Mao et al., 2017, section 6; Niu et al., 2020)

The right-hand column is the practical half of the story. Only the channel and filter rungs run faster on unmodified dense libraries, because the tensor simply shrinks. Kernel and vector sparsity line up with 2-D and 1-D convolution primitives (Winograd, Eyeriss-style 1-D units), which makes custom hardware simpler, but Mao et al. note they are still hard to accelerate on general-purpose processors. Pattern-based pruning needs compiler support (PatDNN) or dedicated support such as NVIDIA's 2:4 sparse tensor cores, and fine-grained sparsity needs accelerators such as EIE (Mao et al., 2017, section 6).

Try every rung on one tensor

The simulator below holds a random [3, 2, 3, 3] tensor and lets you prune it with any of the six units, including the filter unit that Mao et al. list alongside channels. It groups the weights by the chosen unit, scores each group with the L1 or L2 magnitude that part 05 develops, zeros the weakest groups until the target is met, and reports what survives. Watch three things as you switch units: the achieved sparsity snaps to coarser steps, the index count collapses as units grow, and only the channel and filter units ever produce a smaller dense tensor.

SimulatorGranularity pruner: one tensor, six ways to cut it
c_o = 3c_i = 2k_h × k_w = 3 × 3filter 0, input channel 0, kernel 0, row 0, column 0: -0.73filter 0, input channel 0, kernel 0, row 0, column 1: 0.39filter 0, input channel 0, kernel 0, row 0, column 2: 0.15 (pruned)filter 0, input channel 0, kernel 0, row 1, column 0: 0.15 (pruned)filter 0, input channel 0, kernel 0, row 1, column 1: 0.58filter 0, input channel 0, kernel 0, row 1, column 2: -0.19 (pruned)filter 0, input channel 0, kernel 0, row 2, column 0: -0.63filter 0, input channel 0, kernel 0, row 2, column 1: 0.09 (pruned)filter 0, input channel 0, kernel 0, row 2, column 2: 0.36filter 0, input channel 1, kernel 1, row 0, column 0: 0.25 (pruned)filter 0, input channel 1, kernel 1, row 0, column 1: 0.38filter 0, input channel 1, kernel 1, row 0, column 2: 0.05 (pruned)filter 0, input channel 1, kernel 1, row 1, column 0: -0.97filter 0, input channel 1, kernel 1, row 1, column 1: 0.24 (pruned)filter 0, input channel 1, kernel 1, row 1, column 2: -0.16 (pruned)filter 0, input channel 1, kernel 1, row 2, column 0: -0.93filter 0, input channel 1, kernel 1, row 2, column 1: 0.72filter 0, input channel 1, kernel 1, row 2, column 2: -0.45filter 1, input channel 0, kernel 2, row 0, column 0: -0.33filter 1, input channel 0, kernel 2, row 0, column 1: -0.67filter 1, input channel 0, kernel 2, row 0, column 2: 0.29 (pruned)filter 1, input channel 0, kernel 2, row 1, column 0: 0.55filter 1, input channel 0, kernel 2, row 1, column 1: -0.31 (pruned)filter 1, input channel 0, kernel 2, row 1, column 2: -0.47filter 1, input channel 0, kernel 2, row 2, column 0: 0.34filter 1, input channel 0, kernel 2, row 2, column 1: 0.19 (pruned)filter 1, input channel 0, kernel 2, row 2, column 2: -0.02 (pruned)filter 1, input channel 1, kernel 3, row 0, column 0: 0.92filter 1, input channel 1, kernel 3, row 0, column 1: -0.96filter 1, input channel 1, kernel 3, row 0, column 2: 0.70filter 1, input channel 1, kernel 3, row 1, column 0: 0.68filter 1, input channel 1, kernel 3, row 1, column 1: -0.88filter 1, input channel 1, kernel 3, row 1, column 2: 0.68filter 1, input channel 1, kernel 3, row 2, column 0: -0.22 (pruned)filter 1, input channel 1, kernel 3, row 2, column 1: -0.12 (pruned)filter 1, input channel 1, kernel 3, row 2, column 2: -0.19 (pruned)filter 2, input channel 0, kernel 4, row 0, column 0: -0.12 (pruned)filter 2, input channel 0, kernel 4, row 0, column 1: 0.75filter 2, input channel 0, kernel 4, row 0, column 2: -0.11 (pruned)filter 2, input channel 0, kernel 4, row 1, column 0: -0.63filter 2, input channel 0, kernel 4, row 1, column 1: -0.38filter 2, input channel 0, kernel 4, row 1, column 2: -0.48filter 2, input channel 0, kernel 4, row 2, column 0: 0.55filter 2, input channel 0, kernel 4, row 2, column 1: 0.70filter 2, input channel 0, kernel 4, row 2, column 2: 0.78filter 2, input channel 1, kernel 5, row 0, column 0: 0.28 (pruned)filter 2, input channel 1, kernel 5, row 0, column 1: 0.65filter 2, input channel 1, kernel 5, row 0, column 2: 0.66filter 2, input channel 1, kernel 5, row 1, column 0: -0.87filter 2, input channel 1, kernel 5, row 1, column 1: 0.47filter 2, input channel 1, kernel 5, row 1, column 2: -0.56filter 2, input channel 1, kernel 5, row 2, column 0: 0.25 (pruned)filter 2, input channel 1, kernel 5, row 2, column 1: -0.53filter 2, input channel 1, kernel 5, row 2, column 2: -0.85
Unit
Individual weight
Weights per unit
1
Slice
W[o, i, r, c]
Units removed
19 of 54

Units are ranked by the L1 norm of the weights they would remove, lowest first, and pruned until at least 19 of 54 weights are zero. Achieved sparsity snaps to whole units.

Achieved sparsity35.2%19 zerostarget 35%, rounded up to whole units
Remaining parameters35of 54values that still need storing
Index entries35positionsone index per surviving weight
Still 54 slots in memory. The zeros sit inside the tensor, so a dense library computes them anyway; skipping them needs a sparse format and hardware that reads indices.

The energy argument from part 01 also runs along this axis. Mao et al. simulated the SCNN sparse accelerator on VGG-16 and found that, at the same density, vector-level sparsity needs 30 to 35% fewer output memory references than fine-grained sparsity. Neighbouring surviving weights in a row write to the same output address, so SCNN can skip the repeated read and write of that address (Mao et al., 2017, section 6 and Table 4). Since a DRAM access costs about two hundred times a multiply, fewer memory references is where the energy goes, not fewer multiplies.

Recall

Order fine-grained, kernel-level, pattern-based, channel-level and vector-level pruning from irregular to regular.

Fine-grained, pattern-based, vector-level, kernel-level, channel-level.

Quick check

Which granularity turns the weight tensor into a smaller dense tensor that needs no index storage at all?

Recap

If you remember nothing else

  • Granularity is the unit you delete. Smaller units give more index choices, larger units give regular structure.
  • Coarse-grained masks are a subset of fine-grained masks, so at equal accuracy structured pruning reaches equal or lower sparsity, never higher, for the best achievable mask under the same retraining.
  • Fine-grained pruning is hard to accelerate because zeros sit anywhere. Channel or filter pruning yields a smaller dense tensor that any library runs.
  • Convolution weights are [c_o, c_i, k_h, k_w]. Slide 17's example holds 3 x 2 x 3 x 3 = 54 weights.
  • From irregular to regular: fine-grained, pattern-based, vector-level, kernel-level, channel-level.
  • Removing one kernel drops k_h x k_w weights, one input channel drops c_o x k_h x k_w, one filter drops c_i x k_h x k_w.
  • Coarser grains share indices, so their storage penalty is smaller than their sparsity penalty (Mao et al.).

Sources