Part 1 of How to Make Your Model Fast (Part 0: Introduction | Part 2: Inside an Edge Accelerator)
Before you optimise anything, you should be able to state the fastest your model could possibly run and say which of the five hard limits is stopping it. This chapter builds the roofline model from first principles, derives the ridge point, and works through a matmul, a fusion chain and a frame budget with arithmetic you can follow. It covers the two limits that datacentre reasoning ignores and the edge cannot: energy per operation and thermal headroom. You will finish able to write a defensible performance budget for a new project in about fifteen minutes, and to know what it means when the measurement disagrees with it.
There is a question I ask before I write a line of kernel code, and I ask it again before I agree to any latency target: how fast could this possibly go? Not how fast it goes today, and not how fast I hope it will go after a fortnight of work. The ceiling. The number that no amount of cleverness gets past, because it follows from the arithmetic the model must perform and the bytes the machine must move.
Almost every performance project I have watched go badly went badly because nobody wrote that number down first. A team spends three weeks tuning a kernel that was never the bottleneck. Someone commits to 30 frames per second on hardware whose memory bus cannot stream the weights that fast even if the arithmetic units were free. Someone ships a model that passes the bench test and misses its deadline in the field because the enclosure has no fan. All three are avoidable in fifteen minutes with a pencil. This chapter is that pencil.
The standard framing gives three hard limits. At the edge there are five, and the two extra ones are the ones that get people fired.
Compute is operations per second: multiply-accumulate units times clock. It is quoted per precision, because the same silicon area packs more narrow multipliers than wide ones, so a part might do 0.5 TFLOP/s in fp32, 2 TFLOP/s in fp16 and 4 TOPS in int8. Peak means every unit issuing every cycle with no stalls, which never happens, but it is a real upper bound.
Bandwidth is bytes per second between the compute units and wherever the data lives. This is not one number either: there is a bandwidth to the register file, to L1, to the shared on-chip SRAM or L2, and to external DRAM, differing by one to two orders of magnitude at each step. “Memory bandwidth” without qualification usually means the DRAM figure, because that is the slowest and therefore the one that bites.
Capacity matters in two ways that people conflate. The obvious one: does the model fit in RAM at all, weights plus activations plus the runtime’s own footprint. The important one: does the working set of the current kernel fit in on-chip memory? If your tile fits in SRAM, your effective bandwidth is the SRAM number. If it spills by one byte, it is the DRAM number, and you have just lost a factor of ten.
Every operation costs joules. On a mains-powered server that is an accounting problem; on a battery it is a physical constraint with a hard edge, which is that the robot stops. I will show later that inference energy is dominated by data movement rather than arithmetic, so the energy-optimal and latency-optimal designs usually agree, and both say the same thing: move fewer bytes.
Peak clocks are sustainable for a few seconds from cold. A passively cooled SoC in a sealed plastic enclosure might have a sustained envelope of three to five watts while bursting to fifteen. Then dynamic voltage and frequency scaling takes over: the clock drops until the die temperature stabilises, and your compute roof drops with it.
This is the limit I have seen surprise the most people. A harness that runs a model 100 times and reports the minimum latency is measuring a machine that does not exist. Budget instead against the sustained latency after twenty minutes of the real duty cycle, in the real enclosure, at the top of the ambient temperature range in the specification. As a rule of thumb, derate peak throughput by 1.3 to 2x for sustained operation, then go and measure it, because a rule of thumb is not a measurement.
Takeaway: There are five ceilings, not three. Compute, bandwidth and capacity bound what the machine can do; energy and thermal headroom bound what it can keep doing. A design that only respects the first three passes the demo and fails the field trial.
Take any kernel and count two things: the arithmetic operations it must perform, \(F\), and the bytes it must move between the compute units and memory, \(M\). Define the arithmetic intensity:
\[I = \frac{F}{M}\]in operations per byte. This ratio, and not the size of the kernel, determines which limit binds.
The machine gives you two rates: \(C\) operations per second and \(B\) bytes per second. The arithmetic cannot finish in less than \(F / C\) seconds, the data cannot arrive in less than \(M / B\) seconds, and both must happen. If the machine overlaps them perfectly, prefetching the next tile while multiplying the current one, the total time is whichever takes longer:
\[T_{\text{min}} = \max\left(\frac{F}{C},\ \frac{M}{B}\right)\]Divide \(F\) by that time to get achievable throughput, and substitute \(M = F / I\):
\[P = \frac{F}{T_{\text{min}}} = \min\left(C,\ B \cdot I\right)\]That is the roofline.
The two pieces meet where \(B \cdot I = C\), which gives the ridge point:
\[I_{\text{ridge}} = \frac{C}{B}\]This is the most useful single number about a piece of hardware: the intensity a kernel needs before arithmetic rather than memory limits it. Below it you are memory bound and adding compute units changes nothing. Above it you are compute bound and a faster memory bus changes nothing.
Ridge points of representative machines, as peak operations per second divided by peak DRAM bandwidth. Illustrative parameter sets, not measurements of any specific product.
| Machine class | Peak rate | DRAM bandwidth | Ridge point (ops/byte) |
|---|---|---|---|
| Microcontroller, int8, no DRAM | 2 GOPS | 0.4 GB/s (flash) | 5 |
| Mobile SoC CPU, fp32 SIMD | 100 GFLOP/s | 25 GB/s | 4 |
| Mobile SoC NPU, int8 | 4 TOPS | 25 GB/s | 160 |
| Embedded GPU, fp16 | 2 TFLOP/s | 50 GB/s | 40 |
| Datacentre accelerator, bf16 | 400 TFLOP/s | 2000 GB/s | 200 |
Compute has grown far faster than bandwidth for decades, so ridge points have climbed. A machine at 160 ops per byte demands enormous data reuse before its arithmetic units are the constraint, and most real layers at batch one are nowhere near it. This is the central fact of edge inference, and much of the rest of this book is a consequence of it.
The \(\max\) is doing a lot of work. It assumes perfect overlap of computation and data movement, which is reasonable for a large regular kernel on hardware with prefetchers, double-buffered DMA or enough concurrent threads to hide latency, and a bad assumption otherwise. The honest statement brackets the true time:
\[\max\left(\frac{F}{C},\ \frac{M}{B}\right) \ \le \ T \ \le \ \frac{F}{C} + \frac{M}{B}\]The lower end is perfect overlap, the upper end none at all, and where a real kernel sits between them is a property of the code, not the algorithm. Four things break the model in practice:
Takeaway: Compute arithmetic intensity first, compare it to the ridge point, and you know which resource binds before you profile anything. The roofline is a lower bound on time, so a measurement slower than the bound is expected and a measurement faster than the bound means you counted something wrong.
Take a hypothetical edge device with stated parameters: 2 TFLOP/s peak fp32 and 50 GB/s of DRAM bandwidth. Those are plausible for an embedded SoC with a 64-bit LPDDR5 bus, and I state them as assumptions rather than quoting a product. Its ridge point is
\[I_{\text{ridge}} = \frac{2 \times 10^{12}}{50 \times 10^{9}} = 40 \ \text{FLOP/byte}\]Forty floating point operations for every byte that crosses the bus. In fp32, where every element is four bytes, that is 160 operations per element touched. Hold that number in your head.
Now a matrix multiply. Write the activation as \(A\) of shape \(m \times k\) and the weights as \(W\) of shape \(k \times n\), using lowercase for the dimensions so nothing collides with \(M\) for bytes and \(B\) for bandwidth:
\[F = 2 m n k, \qquad M = 4 \cdot (mk + kn + mn)\]taking the ideal case where each matrix is read or written exactly once.
Two matmuls on the same device, one square and one skinny. The device is 2 TFLOP/s and 50 GB/s, ridge point 40 FLOP/byte.
| Quantity | Square: 1024 x 1024 x 1024 | Batch-1 linear: 1 x 1024 x 1024 |
|---|---|---|
| FLOPs | 2.15 G | 2.10 M |
| Bytes (fp32) | 12.58 MB | 4.20 MB |
| Arithmetic intensity | 170.7 FLOP/byte | 0.50 FLOP/byte |
| Time if compute bound | 1.074 ms | 1.05 us |
| Time if memory bound | 0.252 ms | 84.0 us |
| Bound | Compute, 1.074 ms | Memory, 84.0 us |
| Achieved fraction of peak FLOP/s | 100% (at the roof) | 1.25% |
The square matmul sits well above the ridge at 170 FLOP/byte, so it is compute bound and the bus is idle two-thirds of the time. That is the case the datacentre optimises for, and the case in which a better kernel genuinely helps.
The skinny one is more interesting. It does a thousand times less arithmetic but reads almost as many bytes, because the 1024 x 1024 weight matrix has to come in regardless of how many rows you push through it. At 0.5 FLOP/byte, eighty times below the ridge, it runs at 1.25 percent of the machine’s advertised peak. Shown that number without the analysis, most people file a bug against the kernel. There is no bug. The kernel is at the roof; it is just a different roof.
The general case gives a rule you can apply in your head. When \(k\) and \(n\) are large and \(m\) is small, the weight matrix dominates the byte count, so
\[I \approx \frac{2mnk}{s \cdot kn} = \frac{2m}{s}\]where \(s\) is the bytes per weight. The result depends only on the row count \(m\) and the precision. Everything else cancels.
Arithmetic intensity of a weight-dominated matmul depends only on the row count and the weight precision. The right-hand column is the row count needed to reach a ridge point of 40.
| Weight precision | Bytes per weight | Intensity | Rows needed to reach the ridge |
|---|---|---|---|
| fp32 | 4 | \(m / 2\) | 80 |
| fp16 / bf16 | 2 | \(m\) | 40 |
| int8 | 1 | \(2m\) | 20 |
| int4 | 0.5 | \(4m\) | 10 |
At batch one with fp32 weights you would need eighty rows of work per weight load before this device’s arithmetic units become the constraint. You have one. That gap is not something a compiler closes.
Two things follow. First, quantisation buys latency on a memory-bound layer directly, by shrinking the denominator: int8 weights move a quarter of the bytes and the layer gets close to four times faster with no change to the arithmetic units. That is a much bigger effect than the arithmetic speedup usually cited for it, and it is why Part 5 sits where it does in this book. Second, the ridge point moves too, because int8 peak throughput is typically two to four times the fp32 figure on the same part. Both roofs move. Bandwidth almost always wins that race, so quantisation is still a win, but redo the arithmetic rather than assume it.
Matmuls and convolutions are the layers people talk about. They are usually not where the time goes.
An elementwise addition of two fp32 tensors reads eight bytes, writes four and performs one addition: an intensity of \(1/12 \approx 0.083\) FLOP per byte, 480 times below our reference ridge point. A ReLU is 0.125, a fused layer normalisation about 1, a softmax under 1. A gather or embedding lookup does no arithmetic at all, so its intensity is exactly zero, and worse than zero in practice because random access pulls a whole cache line for every scattered element you wanted.
Arithmetic intensity of common operations, fp32, counting compulsory DRAM traffic only. Shapes are stated where the answer depends on them. The reference device has a ridge point of 40 FLOP/byte, so everything below about 40 is memory bound on it.
| Operation | Assumed shape | Operations | Bytes moved | Intensity (FLOP/byte) | Bound by |
|---|---|---|---|---|---|
| Matmul, square | 1024 x 1024 x 1024 | 2.15 G | 12.6 MB | 170 | Compute |
| Matmul, batch 1 | 1 x 1024 x 1024 | 2.10 M | 4.20 MB | 0.50 | Memory |
| Conv 3x3 | 256 to 256 ch, 56 x 56 | 3.70 G | 8.78 MB | 421 | Compute |
| Depthwise conv 3x3 | 256 ch, 56 x 56 | 14.5 M | 6.43 MB | 2.25 | Memory |
| LayerNorm, fused | any | 8 per element | 8 per element | ~1.0 | Memory |
| Softmax, fused | any | ~6 per element | 8 per element | ~0.75 | Memory |
| GELU (tanh approx.) | any | ~10 per element | 8 per element | ~1.25 | Memory |
| ReLU | any | 1 per element | 8 per element | 0.125 | Memory |
| Elementwise add | any | 1 per element | 12 per element | 0.083 | Memory |
| Gather / embedding | any | 0 | 4+ per element | 0 | Memory |
The two convolution rows hold the most instructive result in the table. The depthwise convolution does 256 times fewer FLOPs than the standard one over the same tensor shapes, and moves 73 percent as many bytes. On our device the standard convolution is compute bound at 1.85 ms and the depthwise convolution is memory bound at 129 microseconds, so the real speedup is 14x, not 256x.
A FLOP count is a proxy for latency that holds only above the ridge point, and the operators introduced to cut FLOPs are precisely the ones that fall below it. That is why architectures designed by counting FLOPs so often disappoint on real hardware.
If most operators are bandwidth bound, the time of a region is proportional to how many times you traverse the tensor, because every unfused operator costs at least one read and one write of the whole thing to DRAM.
Take a residual block tail: a convolution, then batch normalisation, then an activation, then an addition with the skip connection. Unfused, that is the conv writing its output, batch norm reading and writing, the activation reading and writing, and the add reading two tensors and writing one. Eight full traversals. Fused into the convolution’s epilogue, with intermediates staying in registers and on-chip memory, it is one read of the skip tensor and one write of the result. Two traversals: a four times cut in DRAM traffic, and so roughly a four times cut in time, from a transformation that changes no numerics at all.
I have never found a transformation with a better ratio of engineering effort to latency won. Before I write a custom kernel, before I quantise, before I touch the architecture, I check what the toolchain actually fused, because the answer is frequently “less than you think”. One unfused reshape or cast between two operators can break a fusion group and quietly cost two tensor traversals. Much of my kernel-level analysis work at Arm has been exactly this: finding the boundaries the compiler refused to cross and working out why. Part 4 is about those refusals. The same reasoning applied to attention is the whole idea behind fused attention kernels: never materialise the attention matrix in DRAM, compute it in tiles on chip, and use a numerically stable running softmax so no separate pass over the row is needed.
Takeaway: In a bandwidth-bound region, time is proportional to the number of tensor traversals, not to the number of operations. Fusing four operators into one turns eight traversals into two, and no amount of arithmetic tuning comes close to that.
Everything above bounds the model. But nobody ships a model; they ship a system with a deadline. On a robot running a perception loop at 30 frames per second the deadline is 33.3 ms, and the model gets a slice of it.
This is the framing I learned to use at Dyson, fitting segmentation and detection networks onto robot hardware. The question was never “how fast is the network”. It was “what is left for the network after everything else the frame has to do”, and the answer was always smaller than people expected.
An illustrative frame budget for a 30 fps perception loop on a mobile robot. The stage times are plausible assumptions to show the shape of the problem, not measurements from any product.
| Stage | Budget | What it is | Can you shrink it? |
|---|---|---|---|
| Exposure and sensor readout | 8 ms | Fixed by the sensor and the light level | Rarely. It is physics and the camera vendor. |
| Transfer, debayer, colour convert | 2 ms | ISP or driver work getting pixels into memory | Sometimes, with a better ISP path |
| Resize, crop, normalise | 1 ms | Preprocessing into model input layout | Yes, and it is often badly done |
| Model inference | 12 ms | Your network | Yes, and this chapter is about how |
| Postprocess: NMS, mask decode | 4 ms | Turning tensors into objects | Yes, frequently the second-biggest win |
| Tracking, fusion, planning | 4 ms | Downstream consumers of the output | Not your call |
| Actuation command | 0.5 ms | Writing to the motor controller | No |
| Jitter and scheduling margin | 1.8 ms | Slack so you do not miss the deadline | You need it. Do not spend it. |
| Total | 33.3 ms |
The model gets 12 ms, about 36 percent of the frame. That is for one model. Run a detector and a segmenter and each gets six. Add a depth network and you are at four each, at which point the runtime’s per-layer dispatch overhead is a material fraction of your budget.
Three refinements matter more than the exact split.
Budget against the tail, not the mean. A model with a 10 ms mean and a 25 ms 99th percentile misses one frame in a hundred, which at 30 fps is once every three seconds. A dropped frame means the control loop extrapolates from stale state, and the failure mode is not “slightly worse metrics”, it is bumping into things. Budget against p99 at sustained clocks and treat the mean as a curiosity.
Latency is not throughput. Pipelining the stages across cores gives 30 fps of throughput while any individual frame takes 60 ms end to end. For a display that is fine. For a robot moving at one metre per second, 60 ms of staleness is six centimetres between what the model saw and where the machine now is. Decide which your product needs and write it down, because the two lead to different designs.
The budget is a veto, not a target. If the roofline bound alone exceeds the slice, stop: no kernel work will save you, and you must change the model, the input resolution or the precision. The earlier you discover that, the cheaper it is.
Takeaway: The model typically gets a third of the frame, measured at the 99th percentile, at sustained clocks, sharing the machine with everything else. Compare the roofline bound against that slice before committing to an architecture, not after.
The relative energy costs of arithmetic and data movement have been widely cited since Horowitz’s 2014 keynote on computing’s energy problem, and reproduced in the model compression literature since. The figures below are order-of-magnitude values for a 45 nm process. Absolute numbers have improved on newer nodes, but the ratios, which are what matter for design, have barely moved, because wires have not scaled the way transistors have.
Approximate energy per operation, 45 nm, normalised to an int8 multiply-accumulate. Treat these as orders of magnitude, not measurements of any particular chip.
| Operation | Approximate energy | Relative to an int8 MAC |
|---|---|---|
| int8 add | 0.03 pJ | 0.1x |
| int8 MAC | ~0.2 pJ | 1x |
| fp16 MAC | ~1.5 pJ | ~7x |
| fp32 add | 0.9 pJ | ~5x |
| fp32 multiply | 3.7 pJ | ~18x |
| fp32 MAC | ~4.6 pJ | ~20x |
| 32-bit register file read | ~1 pJ | ~5x |
| 32-bit read, small on-chip SRAM | ~5 pJ | ~25x |
| 32-bit read, 1 MB on-chip SRAM | ~100 pJ | ~500x |
| 32-bit read from DRAM | 640 to 2600 pJ | ~3,000 to 13,000x |
Read the bottom row against the middle and the whole discipline of efficient inference falls out of it. Fetching one 32-bit weight from DRAM costs roughly as much energy as three thousand int8 multiply-accumulates.
So if a weight is used fewer than about three thousand times after you fetch it, the energy of your inference is in the fetch, not the arithmetic. Arithmetic is free; memory is the bill. That is why energy-optimal and latency-optimal designs point the same way, and why the two big edge techniques, quantisation and fusion, both work by moving fewer bytes rather than by doing less maths.
A sanity calculation: 640 pJ per 4 bytes is about 160 pJ per byte, so a model moving 40 MB per inference spends at least \(40 \times 10^{6} \times 160 \times 10^{-12} \approx 6.4\) mJ on traffic alone, about 0.19 W at 30 inferences per second. That is a floor, excluding on-chip SRAM, control logic, clock distribution and leakage, which in a real part often add up to several times it.
Datacentre inference batches, because batching is free money: fetch a weight once, use it across 64 rows, and intensity goes up 64-fold. Reasoning that starts from batched serving concludes that you should care about arithmetic units and FLOP efficiency, and for that workload it is correct.
At the edge there is one camera, one frame, one user, one utterance. Batch size is one for a reason you cannot engineer away: the next frame does not exist yet, and waiting for it adds 33 ms of latency to buy throughput you do not need. So every weight byte is fetched and used for exactly one multiply-accumulate per output it contributes to, nothing is amortised, and for any layer whose weights dominate its byte count, inference time collapses to one expression:
\[T \approx \frac{\text{weight bytes}}{B_{\text{effective}}}\]This is the most useful back-of-envelope formula in edge machine learning. A 7B-parameter model quantised to int8 is 7 GB of weights, so on a 50 GB/s device it cannot decode faster than \(7 / 50 = 140\) ms per token, about 7 tokens per second, or 5 at a realistic 70 percent of peak. Quantise to int4 and it doubles. Buy a faster processor on the same memory bus and it does not move at all.
One exception separates vision from language at the edge. Convolution reuses each weight across every spatial position: a 3x3 kernel on a 56 x 56 feature map uses each weight 3,136 times, so a CNN’s heavy layers keep high intensity even at batch one. Transformer decode has no such reuse, since each weight serves one token. That is why a CNN at batch one can be compute bound and a language model at batch one essentially never is.
Takeaway: Data movement costs roughly a thousand times more energy than arithmetic, and at batch one nothing is amortised, so for weight-dominated workloads both time and joules are set by total weight bytes divided by effective bandwidth. Optimise the bytes.
Here is the procedure. It takes about a quarter of an hour and I run it at the start of every project, before any code gets written.
Most of this fits in a script you write once per device.
# A roofline calculator. Fill in the device numbers once per project.
PEAK_OPS = 2.0e12 # fp32 ops/s, datasheet
PEAK_BW = 50e9 # DRAM bytes/s, datasheet
COMPUTE_EFF = 0.60 # what a large tuned GEMM actually reaches
BW_EFF = 0.70 # what a streaming copy actually reaches
C = PEAK_OPS * COMPUTE_EFF
B = PEAK_BW * BW_EFF
RIDGE = C / B
def bound(ops, byts):
t_compute = ops / C
t_memory = byts / B
return {
"intensity": round(ops / byts, 2),
"t_compute_ms": round(t_compute * 1e3, 3),
"t_memory_ms": round(t_memory * 1e3, 3),
"floor_ms": round(max(t_compute, t_memory) * 1e3, 3),
"limit": "compute" if t_compute > t_memory else "memory",
"energy_floor_mj": round(byts * 160e-12 * 1e3, 3),
}
def matmul(m, n, k, wbytes=4):
return 2 * m * n * k, wbytes * (m * k + k * n + m * n)
print(f"effective ridge point: {RIDGE:.1f} ops/byte")
for name, shape in [("square 1024", (1024, 1024, 1024)),
("batch-1 linear", (1, 1024, 1024))]:
print(name, bound(*matmul(*shape)))
It will. That is the point of having a model: the size of the disagreement tells you what kind of problem you have. Here is how I read the ratio of measured time to the roofline floor.
Diagnosing the gap between measured latency and the roofline floor. The ratio is measured time divided by the bound from step 3.
| Ratio | What it usually means | Where to look |
|---|---|---|
| Below 1.0 | Your model is wrong, not your code | Timer not synchronised on async hardware; byte count assumed DRAM traffic that was cached; FLOP count too high because of sparsity or constant folding |
| 1.0 to 1.3 | You are at the roof | Stop tuning. Change the algorithm, the precision or the shape |
| 1.3 to 2 | Ordinary inefficiency | Imperfect overlap, tail effects in tiling, suboptimal vectorisation |
| 2 to 5 | You are moving more bytes than you counted | Layout conversions inserted by the runtime, unfused elementwise chains, weight re-reads because a tile does not fit, cache conflicts, unaligned access |
| 5 to 50 | You are not bandwidth bound at all, you are overhead or latency bound | Per-layer dispatch cost, synchronisation, an operator falling back to a reference CPU implementation, dynamic shapes forcing re-planning, allocation in the hot loop |
| Above 50 | Something is structurally wrong | The accelerator is not being used; the graph is running in the framework’s eager path; data is being copied between address spaces every layer |
The first row is the one people get wrong. If you measure faster than your own lower bound you have not found a fast machine, you have a bug in your measurement or your accounting, and I have wasted whole afternoons celebrating one before finding it. Suspect the timer first: on asynchronous hardware, a timer that does not synchronise measures how long it took to enqueue the work.
The discipline I have ended up with is simple. Never optimise a kernel before you can state its roofline bound and the ratio of measured to bound. If you cannot say “we are at 22 percent of the memory roof on this layer”, you do not yet know what you are doing, and any speedup you find is luck. Luck does not generalise to the next model.
Takeaway: The model is a measuring instrument, and the gap between it and reality is the diagnostic. A 1.2x gap means stop; a 3x gap means find the bytes you did not count; a 20x gap means you are paying overhead, not bandwidth.
Problem 1. A mobile NPU is specified at 4 TOPS int8 with 25 GB/s of DRAM bandwidth. Find its ridge point. Then take a batch-1 fully connected layer of 2048 inputs to 2048 outputs with int8 weights and int8 activations. Compute its arithmetic intensity, its bound, and the fraction of peak throughput it achieves. Assume ideal traffic and count a MAC as two operations.
Ridge point: \(4 \times 10^{12} / 25 \times 10^{9} = 160\) ops per byte.
Operations: \(2 \times 1 \times 2048 \times 2048 = 8.39\) M ops.
Bytes: weights \(2048 \times 2048 \times 1 = 4.194\) MB, plus input 2,048 bytes and output 2,048 bytes, so 4.199 MB. The weights are 99.9 percent of it.
Intensity: \(8.39 \times 10^{6} / 4.199 \times 10^{6} = 2.0\) ops per byte. This matches the rule \(I \approx 2m / s\) with one row and one byte per weight. It is 80 times below the ridge, so firmly memory bound.
Compute time: \(8.39 \times 10^{6} / 4 \times 10^{12} = 2.1\) us. Memory time: \(4.199 \times 10^{6} / 25 \times 10^{9} = 168\) us. Bound: 168 us, memory.
Achieved throughput: \(8.39 \times 10^{6} / 168 \times 10^{-6} = 50\) GOPS, which is 1.25 percent of the 4 TOPS peak. The chip spends 99 percent of the time waiting for weights. Buying a part with twice the TOPS and the same bus would change this layer’s latency by nothing at all.
Problem 2. A residual block tail produces a tensor of shape 1 x 64 x 112 x 112 in fp16, then applies batch normalisation, then ReLU, then an elementwise add with a skip connection of the same shape. Count the DRAM traffic unfused and fused, on a device with 50 GB/s of effective bandwidth. How much time does fusion save per block, and per network if there are sixteen such blocks?
Elements per tensor: \(64 \times 112 \times 112 = 802{,}816\). At 2 bytes each, one traversal is 1,605,632 bytes, about 1.606 MB.
Unfused traversals: conv writes its output (1), batch norm reads and writes (2), ReLU reads and writes (2), add reads two tensors and writes one (3). Total 8 traversals.
\(8 \times 1.606 = 12.85\) MB, taking \(12.85 \times 10^{6} / 50 \times 10^{9} = 257\) us.
Fused, with the batch norm folded into the convolution weights and the ReLU and add done in the convolution’s epilogue while the tile is still on chip: read the skip tensor (1), write the final output (1). Total 2 traversals.
\(2 \times 1.606 = 3.21\) MB, taking 64 us.
Saving: \(257 - 64 = 193\) us per block, a 4x reduction. Over sixteen blocks, \(16 \times 193 = 3.09\) ms.
For context, that saving alone is a quarter of the 12 ms model slice in the frame budget table, and it required no change to the model, the weights or the numerics.
Problem 3. A 3B-parameter decoder-only language model is quantised to int4 weights. It has 26 layers, a model dimension of 3072, and grouped-query attention with 8 key-value heads of dimension 96, with the KV cache kept in fp16. The device has 68 GB/s of peak bandwidth and achieves 70 percent of it. Find the decode throughput ceiling at short context, and again at a context of 4,096 tokens.
Effective bandwidth: \(0.70 \times 68 = 47.6\) GB/s.
Weights: \(3 \times 10^{9} \times 0.5 = 1.5\) GB.
At short context the KV cache is negligible, so time per token is \(1.5 \times 10^{9} / 47.6 \times 10^{9} = 31.5\) ms, giving about 31.7 tokens per second.
KV cache per token: the K and V vectors are each \(8 \times 96 = 768\) values, so 2 tensors times 768 values times 2 bytes = 3,072 bytes per layer, times 26 layers = 79,872 bytes, about 78 KB per token.
At 4,096 tokens of context: \(4096 \times 79{,}872 = 327\) MB, which must be read in full on every decode step.
Extra time per token: \(0.327 \times 10^{9} / 47.6 \times 10^{9} = 6.9\) ms.
Total: \(31.5 + 6.9 = 38.4\) ms per token, about 26 tokens per second, a 19 percent slowdown purely from context length.
Note that none of this depends on how fast the arithmetic units are, and that without grouped-query attention the cache would be four times larger and the penalty 27.5 ms per token, roughly halving throughput.
Problem 4. A compute-bound vision model measures 9.5 ms on the bench from cold. In the product enclosure at maximum ambient temperature, the sustained clock settles at 72 percent of the benchmark clock, and the p99 latency is 1.35 times the mean. The frame budget allocates the model 12 ms. Does it fit? What speedup do you actually need?
The model is compute bound, so latency scales inversely with clock. Sustained mean: \(9.5 / 0.72 = 13.2\) ms. That already exceeds the 12 ms slice, before considering the tail.
Sustained p99: \(13.2 \times 1.35 = 17.8\) ms. It misses the deadline on one frame in a hundred by 48 percent, which at 30 fps is a missed frame every 3.3 seconds.
To fit p99 inside 12 ms, the sustained mean must be at most \(12 / 1.35 = 8.89\) ms, which corresponds to a bench-clock mean of \(8.89 \times 0.72 = 6.40\) ms.
Required speedup against the number you originally measured: \(9.5 / 6.40 = 1.48\)x.
So the naive reading of the bench number says you are 25 percent under budget and done. The correct reading says you need close to a 1.5x speedup. That factor of difference between “ship it” and “another month of work” is entirely an artefact of benchmarking a cold machine and reporting a mean, and it is the single most common way I have seen an edge deployment schedule slip.
You can now bound a workload before writing code: count the operations and the bytes, form the intensity, compare it to the ridge point, take the maximum of the two times, and check the result against the slice of the frame budget and the energy envelope you actually have. That gets you the right question. It does not yet tell you why the machine behaves the way it does, or why the effective bandwidth is 70 percent rather than 100, or where the on-chip memory that makes fusion possible actually lives. For that we have to open the box.