Convolution on cells of different sizes

A fire simulation (FDS) splits space into small blocks of cells. Blocks near the house use small cells and blocks far away use big ones. This page uses a 2D picture whose pixels come in three sizes to show the ways a convolution can run on that kind of grid.

Short answer. A convolution counts in cells, not metres: a 3×3 kernel reads the cell itself and its 8 neighbours.
  1. Inside one block every cell has the same size, so the block is an ordinary small image and the kernel works as usual.
  2. Sizes differ only across a block's edge. Before each layer, give every block a one-cell border (a halo) holding its neighbours' values, converted to this block's cell size: average them if the neighbour's cells are smaller, copy the value if they are bigger.
  3. The cell size is fed in as an extra input channel, so the same weights know whether one step means 1 m or 4 m.
Hover over any figure below to see exactly what a kernel reads.

The toy grid

Hover over a cell to see its size and block.

A 48 × 48 m domain, cut into blocks of 4 × 4 cells, the same way FDS uses blocks of 16 × 16 × 16 in 3D.

The colours show a made-up temperature: a fire next to the house (bottom middle), a plume rising and leaning right, and a small hot spot (right of the fire) that only the 1 m cells can resolve.

The sizes meet in three ways: 1 m next to 2 m, 2 m next to 4 m, and 1 m directly next to 4 m (a jump of 4×). Every approach below has to handle all three.

A · Resample everything to the finest grid simple, far too big in 3D

Hover: the 3×3 kernel on the 1 m pixel grid.

Cut every cell into 1 m pixels, copying the value into each one, then run a normal CNN on a 48 × 48 image.

Good

  • Standard image CNN, nothing custom.

Catch

  • Most pixels are copies: they hold no information FDS computed, and the network spends nearly all its compute on them.
  • In 3D the ratio is far worse, since each size jump costs 8×, not 4×: 0.25 m everywhere is ~168 M cells, against 1–4 M real FDS cells. That is about 1,300 GiB of activations per training step.
  • The loss would ask the model for fine detail that the labels don't contain.

B · Resample everything to the coarsest grid cheap, throws away the house

Hover: the 3×3 kernel on the 4 m pixel grid.

Average the small cells into 4 m pixels and run a normal CNN on a 12 × 12 image.

Good

  • Standard CNN, tiny.

Catch

  • The hot spot next to the house is averaged away. In the real problem, the heat on the house walls is decided at exactly that scale.
  • It is still useful as the coarse level of a U-Net (approach C or D), not as the whole model.

C · Blocks + halo candidate / baseline

Hover over a block. Its halo (dashed) is coloured by how each halo cell is filled; the kernel on the hovered cell is outlined in black.

Keep FDS's blocks as they are. Each block is a small regular image at its own cell size. Before every conv layer, wrap each block in a one-cell halo filled from its neighbours, converted to this block's cell size:

same size: copy the neighbour cell neighbour finer: average the small cells under it neighbour coarser: copy the big cell it sits in outside the domain: boundary value

After that, every block is just a 6 × 6 image (right), whatever its cell size. All blocks of the same shape are stacked into one batch and run through one ordinary conv2d (conv3d in 3D). The cell size goes in as an extra channel.

Good

  • Keeps every real FDS cell and nothing else; the halo is rebuilt each layer and thrown away.
  • Dense convs on GPU tensor cores, and no per-neighbour tensors to store. In 3D, about 13–33 GiB per step for the gas (estimate), against ~110–215 for a GNN.
  • Which cells fill each halo cell is fixed per scene, so it is precomputed once as a gather map.

Catch

  • The halo map is custom code: one gather per layer.
  • The kernel reaches 1 cell, so 1 m in a fine block and 4 m in a coarse one. Reach over distance comes from coarse U-Net levels, not from this layer.

Stacking layers: convolutions of convolutions

One layer reads 1 cell in each direction. Stacking layers repeats that, and the halo is refreshed before every layer from the neighbouring blocks' current features, not from the raw input. Information therefore crosses a block edge one cell per layer, and the "average if finer, copy if coarser" rule is applied to features at every layer.

1D cross-section: what a cell depends on through the layers

reads a same-size cell reads the average of smaller cells reads a copy of a bigger cell

Hover over any cell. Each row is one layer's output, computed from three positions in the row below. The highlighted cone is everything in the input (bottom row) that can influence that cell. The cone is a fixed number of cells wide on each side, so in metres it is lopsided: it reaches further on the side where cells are big.

2D: how far a cell sees after L layers

Hover over a cell. Shading shows which layer first brings each input cell into view (darker = later). The dashed square is what the same number of layers covers on a uniform 1 m grid (approach A).

Layers L = 3

What this means

  • Reach per layer is one local cell: 1 m among the 1 m cells and 4 m among the 4 m cells. FDS chose small cells where things change fast, so the network looks closer exactly where detail matters.
  • Crossing into a bigger-cell region, reach in metres grows faster. Crossing into a smaller-cell region, the averaged halo hands over a summary of several small cells at once.
  • Plain average and copy are fixed rules. Since features are not physical quantities, they can be swapped for learned transfers: a stride-2 conv from fine to coarse and a transposed conv from coarse to fine. That costs a few weights per size pair, and the conservation step (refluxing) is unchanged.
  • Going 25 m in one 0.5 s step would take 25 layers among 1 m cells. That is what the coarse U-Net levels are for: pool to 4 m and 16 m grids, take a few layers there, and bring the result back.

One layer, in code

for layer in layers:
    halo = gather(features, halo_map)  # copy or average
    x = pad_with(features, halo)       # (n_blocks, C, 6, 6)
    features = act(norm(conv(x)))     # one batched conv

H · Kernel counted in finest cells (OctNet) evens the weights, not the reach

OctNet (Riegler, Ulusoy and Geiger, CVPR 2017) defines a convolution on mixed-size cells this way: treat every big cell as a block of identical finest cells, run an ordinary kernel on that finest grid, then average the result back to one value per big cell. A big cell's interior is computed once, so the cost is close to C's, and the paper shows the result equals the full fine-grid convolution. In C's block form it means each cell size uses a kernel derived from the one finest-cell kernel instead of its own weights, and one kernel step always means one finest cell.

Layers L = 4

Same 1D row as the cone above. Hover over a cell. Bars show how much each metre of input weighs in that cell's output after L layers, with every kernel weight set to 1/3; all three rows share one scale. The bracket under each row spans every input the cell depends on at all.

What this means

I · Mixed depth: fine cells get extra layers first candidate

Run the first layers on the finest cells only, while bigger cells wait with their values unchanged. Then pool the fine cells up one size, where they join the native cells of that size, and run the next layers on that level. Repeat until the whole domain is one grid at the coarsest size. Every layer convolves cells of one size; sizes meet only through pooling. The decoder mirrors this on the way back (unpool, add the skip, run layers), so this is a U-Net whose first levels are FDS's own cell sizes.

reads a same-size cell pooling: averages smaller cells seam: reads a bigger cell that is still waiting waiting: value carried unchanged

Same 1D row as the cone above, two layers per level (encoder only). Faded boxes are waiting. Hover over any box to see everything in the input it depends on.

The same schedule on the 2D toy grid

Good

Catch

Prior art: per-region depth chosen by a learned gate (Spatially Adaptive Computation Time, CVPR 2017; Dynamic Convolutions, CVPR 2020; Mixture-of-Depths, 2024); depth tied to octree level (Adaptive O-CNN, 2018); a fine stream that starts first with coarse streams joining later (HRNet, 2019); fine levels taking more steps (Berger–Oliger subcycling in adaptive-mesh CFD). Here the depth is fixed by the mesh, not learned.

D · One image per level (patch-based AMR) close alternative to C

Outlines show each level's rectangle. Hatched 4 m cells sit under the finer levels ("shadow" cells holding the average of the cells above them).

Group all cells of one size into one rectangle (its bounding box) and treat each rectangle as one image, with a ghost ring filled the same way as C's halo. The coarsest level covers the whole domain, including under the finer patches.

Good

  • Few, large images: fewer conv calls and fewer halo cells than C.
  • The coarse level is already a full-domain image, which is the first level of the U-Net.

Catch

  • If one size's cells don't form a rectangle (an L-shape, say), the bounding box contains padding. In FDS that depends on how box-shaped each scene's refinement is.

E · Look up each neighbour on the fly (sparse / octree conv) same rule as C, no blocks

Hover over any cell: each of the 9 kernel positions is drawn at the cell's own size and coloured by how it is filled.

Skip the blocks. For every cell, place the 3 × 3 kernel at the cell's own size and look up what sits under each of the 9 positions, with the same rule as C (copy, average or boundary). Libraries such as spconv, TorchSparse and MinkowskiEngine do this with hash tables, and octree CNNs (O-CNN) do it on quadtrees and octrees.

same size average of finer cells copy of a coarser cell outside

Good

  • No halo storage; works for any layout.

Catch

  • Most sparse-conv libraries assume one cell size per level, so the cross-size averaging is still ours to write.
  • Gathers are slower than C's dense block convs. C does the same thing, grouped into regular blocks.

F · Graph network (message passing) the earlier plan

Hover over a cell to see its edges. Every line is an edge that stores its own hidden vector during training.

Each cell is a node, and each pair of cells sharing a face is an edge, whatever their sizes. A layer computes a message on every edge with a small MLP, then each node sums its incoming messages.

Good

  • Handles any sizes and any shapes with no special rules. A big cell simply has more neighbours.
  • The natural fit for walls and vegetation particles, which are not on a grid.

Catch

  • Training stores a hidden vector on every edge, per layer. In 3D a cell has about 6 face edges, which is why activation memory is about 6–8× a CNN on the same cells (~110–215 GiB per step vs ~13–33).
  • Scatter and gather are memory-bound: slower than dense convs on a GPU.

G · Kernel defined in metres (continuous / point convolution) flexible, uneven cost

Hover over a cell: every cell centre within the radius is a neighbour. Darker = closer, so a larger weight.

Define the kernel as a function of physical offset in metres. Every cell centre within R metres is a neighbour, weighted by a small network applied to the offset (as in KPConv and continuous convolutions).

Radius R = 3.0 m

Good

  • One rule for any sizes, positions or point clouds.

Catch

  • The neighbour count swings with cell size: a radius that suits a 4 m cell sweeps in dozens of 1 m cells near the house.
  • Each neighbour pair stores an intermediate tensor, as in a GNN, so memory is at least as high.

Keeping mass and energy exact where sizes meet

The model updates each cell by what flows through its faces: new amount = old amount + inflow − outflow. If every face's flow is counted once, as outflow on one side and inflow on the other, nothing appears or vanishes.

Where a 2 m cell meets two 1 m cells, the big cell's single face lines up with two small faces. If each side computes its own flux, they disagree, and the difference leaks.

Refluxing is the standard fix in adaptive-mesh CFD. The pairing of each big face with its small faces is fixed per scene, so it is precomputed alongside the halo map. It applies in the same way to C, D and E.

Comparison

ApproachToy: values storedToy: neighbour slots per layer3D fire scene (estimates)Main catch

"Neighbour slots" counts what a layer reads (kernel taps or edges). For C, D and E these are reads only; for F and G every slot also stores a training tensor. The 3D column uses rough per-step training-memory estimates for one scene (128 channels, bf16), not measurements.