HomeFPGA Neural NetworkDay 8 — Pooling & Normalization

Pooling & Normalization

Max/average/global pooling hardware, batch normalization folding into conv weights, and the fused BN+ReLU+Pool pipeline that runs the whole stage at one pixel per clock.

By EcrioniX Engineering Team · Published June 15, 2026 · ~4,600 words · 14 min read

1. Where Pooling and Norm Fit

After convolution and activation, two more operations shape the feature map: pooling (which shrinks spatial dimensions) and normalization (which stabilizes the data distribution). Both are cheap in hardware compared to convolution — and both are usually fused into the conv pipeline so they cost nothing extra in memory traffic.

The Standard CNN Block
Conv BatchNorm ReLU Pooling next layer BN folds into Conv · ReLU+Pool fuse → one streaming pipeline

2. Max Pooling

Max pooling takes the maximum value in each window (typically 2×2, stride 2), halving the width and height. It keeps the strongest activation and discards the rest — adding translation invariance. In hardware it's just comparators, no multipliers.

2×2 Max Pooling (stride 2)
Input 4×4 3 9 1 5 2 4 2 3 8 1 6 7 4 3 5 2 → max → Output 2×2 9 5 8 7 Each 2×2 quadrant → its maximum · 4×4 → 2×2 (4× fewer pixels)
// maxpool2x2.v — Streaming 2×2 max pooling (stride 2) module maxpool2x2 #(parameter IMG_W = 224, parameter DW = 8)( input wire clk, rst_n, input wire pix_valid, input wire signed [DW-1:0] pix_in, output reg signed [DW-1:0] pool_out, output reg out_valid ); reg signed [DW-1:0] lb [0:IMG_W-1]; // 1 line buffer (prev row) reg signed [DW-1:0] prev_col; // previous pixel this row reg [$clog2(IMG_W):0] col; reg row_odd; // toggles each row // 3-comparator max tree for the 2×2 window function signed [DW-1:0] max2(input signed [DW-1:0] a, b); max2 = (a > b) ? a : b; endfunction always @(posedge clk or negedge rst_n) begin if (!rst_n) begin col <= 0; row_odd <= 0; out_valid <= 0; end else if (pix_valid) begin // store this pixel into the line buffer for next row's pairing lb[col] <= pix_in; prev_col <= pix_in; // produce one output every 2 cols on odd rows (2×2 complete) if (row_odd && col[0]) begin // window = {lb[col-1], lb[col], prev_col, pix_in} pool_out <= max2( max2(lb[col-1], lb[col]), max2(prev_col, pix_in) ); out_valid <= 1; end else out_valid <= 0; if (col == IMG_W-1) begin col <= 0; row_odd <= ~row_odd; end else col <= col + 1; end end endmodule

3. Average Pooling

Average pooling outputs the mean of each window. Slightly more hardware than max pooling — an adder tree plus a divide — but the divide by a power of 2 (e.g. /4 for 2×2) is just a right shift, so it's still multiplier-free.

2×2 Average Pool: out = (a + b + c + d) / 4 = (a + b + c + d) >> 2 ← divide-by-4 is a shift! Hardware: 3 adders + 1 shifter (no multiplier, no divider) Max vs Average pooling: Max pool → keeps strongest feature, sharper, default for CNNs Avg pool → smoother, used in some architectures + final GAP

4. Global Average Pooling (GAP)

GAP collapses each entire channel into a single number by averaging all its pixels. Modern CNNs (ResNet, MobileNet, EfficientNet) use GAP instead of huge fully-connected layers — slashing parameters and overfitting.

Global Average Pooling — Channel → Scalar
7×7 channel 49 pixels Σ/49 μ 1 value/channel ResNet-50: replaces a 2048×7×7 FC layer → 100M fewer params
Hardware: one accumulator per channel + a final multiply by the reciprocal (1/N precomputed). Streams at one pixel/clock.

5. Batch Normalization — and Why It Disappears

Batch normalization stabilizes training by normalizing each channel. At inference time, all four BN parameters (mean μ, variance σ², scale γ, shift β) are frozen constants — which means the entire BN operation is a per-channel affine transform: y = γ·(x−μ)/√(σ²+ε) + β.

Because it's affine and the previous layer (convolution) is also affine, the two can be merged — this is BN folding.

Batch Norm (inference) = per-channel affine: y = γ·(x − μ)/√(σ²+ε) + β = a·x + b where a = γ/√(σ²+ε), b = β − a·μ BN FOLDING into the preceding conv (weight W, bias B): conv output: x = W·in + B after BN: y = a·(W·in + B) + b = (a·W)·in + (a·B + b) → new_weight = a · W → new_bias = a · B + b Result: BN vanishes. ZERO extra hardware at inference. Folding is done offline, once, before deployment.
BN Folding — Two Layers Become One
Training (2 layers) Conv BatchNorm Inference (folded) Conv (new W, b) BN absorbed The BN layer is gone — conv weights/bias are pre-scaled offline. No runtime cost.

Folding Only Works at Inference

BN folding is valid because the BN statistics are frozen after training. During training, μ and σ² change every batch, so BN must stay separate. This is purely an inference-time optimization — and it's why you almost never see a standalone BatchNorm block in a deployed FPGA accelerator.

6. Fused BN + ReLU + Pool Pipeline

The whole post-conv stage collapses into a single streaming pipeline. BN is already folded into conv, so the engine only needs requantize → ReLU → pool, all in registered stages with no memory round-trips.

// fused_relu_maxpool.v — requantize → ReLU → 2×2 max pool (BN pre-folded) module fused_relu_maxpool #(parameter ACC_W=32, parameter DW=8, parameter IMG_W=224)( input wire clk, rst_n, input wire valid_in, input wire signed [ACC_W-1:0] acc_in, // conv accumulator (BN already folded in) input wire [7:0] req_shift, // requantize shift output reg signed [DW-1:0] y, output reg valid_out ); // Stage 1: requantize + ReLU + saturate to INT8 reg signed [DW-1:0] act; reg act_valid; always @(posedge clk) begin if (!rst_n) begin act <= 0; act_valid <= 0; end else if (valid_in) begin reg signed [ACC_W-1:0] s; s = acc_in >>> req_shift; // requantize s = s[ACC_W-1] ? 0 : s; // ReLU act <= (s > 127) ? 8'sd127 : s[DW-1:0]; // saturate act_valid <= 1; end else act_valid <= 0; end // Stage 2: feed activated stream into the 2×2 max-pool engine maxpool2x2 #(.IMG_W(IMG_W),.DW(DW)) u_pool ( .clk(clk), .rst_n(rst_n), .pix_valid(act_valid), .pix_in(act), .pool_out(y), .out_valid(valid_out) ); endmodule

7. Resource & Throughput Summary

OperationHardwareMultipliersThroughput
Max pool 2×23 comparators + 1 line buffer01 out / 4 in
Avg pool 2×23 adders + shifter01 out / 4 in
Global avg pool1 accumulator/channel + reciprocal1 (×1/N)1 pixel/clock
Batch norm (folded)none — merged into conv0free
Fused ReLU+Poolshifter + comparators01 pixel/clock

The Big Picture

Convolution (Day 5) does the heavy lifting with thousands of MACs. Everything in this lesson — pooling, normalization, activation — is essentially free by comparison: no multipliers, fused into the stream. This is why CNN accelerators spend ~95% of their DSPs on convolution and almost none on these layers.

Day 8 — Key Takeaways

Next — Day 9: Pipelining & Parallelism — layer pipelining, inter-layer FIFOs, the throughput-vs-latency trade-off, and data vs model parallelism for the full CNN.

← Previous
Day 7: Activation Functions
Next →
Day 9: Pipelining & Parallelism