A Worked Optimization Example, Common Mistakes, and the Performance Equation
Part 18 of 19
Ten lessons have each dug into one piece of the inference-optimization stack in isolation — quantization, fusion, memory planning, kernels, compilers, compression, profiling, hardware. This lesson puts them back together. It stacks six of those techniques on a single model to see how they compound, catalogs the eight mistakes that most commonly undo the gains, breaks total latency into five terms so "where did the time go" has a precise answer, and closes with the order in which a practitioner should actually attack the problem.
1. One Model, Six Optimizations: A Worked Example
Every number in this section is illustrative — invented for pedagogical consistency, not measured on real hardware — but the shape of the compounding, and the mechanism behind each step, is exactly what shows up in real optimization work. Suppose a small CNN — conv, ReLU, conv, batch norm, ReLU, a final GEMM — starts life the way most models do the first time anyone runs it end to end: float32 weights and activations, executing entirely on a CPU, every operator its own dynamically-allocated buffer, every intermediate tensor written to and read back from DRAM, no fusion, and generic (non-vectorized, non-blocked) kernels underneath. Call its baseline latency 100 ms.
1.1 Profiling the Baseline First
Before touching anything, Lesson 9's discipline says: profile before you optimize, because intuition about where time goes is wrong more often than it's right. Suppose a profiler breaks that 100 ms down using the five-term equation this post formalizes in Section 3:
Compute (arithmetic on CPU, generic kernels) 55 ms
Memory (DRAM traffic for unfused intermediates) 30 ms
Launch overhead (dynamic allocation, per-op dispatch) 10 ms
Synchronization (single device, minimal) 3 ms
Transfer (single device, negligible) 2 ms
------------------------------------------------------------
Total 100 msTwo things about this breakdown matter for everything that follows. First, compute is the largest single term, but it is not the only term — 45 of the 100 ms is memory traffic, launch overhead, and synchronization, none of which a naive "count the FLOPs" analysis would even notice. Second, this breakdown is what makes the next six steps legible: each optimization below is not just "a number went down," it is a specific, nameable attack on one or more of these five terms, and by the end of Section 1 the same breakdown reappears radically smaller.
1.2 Optimization 1: INT8 Quantization — 100 ms to 70 ms
Converting weights and activations from float32 to int8 is the subject of Lesson 3, in both its affine/symmetric fundamentals and its calibration and QAT mechanics. On hardware with native low-precision SIMD or dot-product instructions, int8 multiply-accumulates are cheaper per element than float32 ones, and four times fewer bytes need to move per tensor. Suppose this step takes the model from 100 ms to 70 ms — a 30 ms saving, split mostly between compute (cheaper MACs) and memory (smaller tensors), assuming the favorable case: every operator on the model's critical path has a real int8 kernel, and the graph doesn't yet interleave supported and unsupported ops. That assumption is doing real work, and Section 2's Mistake 4 comes back to exactly what happens when it's false.
1.3 Optimization 2: Operator Fusion — 70 ms to 60 ms
Lesson 2 covers fusing Conv and ReLU into a single kernel so the intermediate activation never round-trips through DRAM between the two ops — it stays resident in registers or on-chip cache for the handoff. This is a direct attack on two of the five terms at once: less memory traffic (the intermediate tensor's read-then-write pair is gone), and fewer kernel launches (one fused kernel replaces two separate dispatches). Suppose fusing the two Conv-plus-activation pairs in this network saves 10 ms, bringing the model to 60 ms.
1.4 Optimization 3: Memory Planning — 60 ms to 55 ms
Lesson 5, Part 1 reuses activation buffers across operators instead of allocating a fresh buffer for every intermediate tensor, using liveness analysis to figure out which buffers can safely alias each other. This mostly attacks launch overhead — a static, pre-planned arena means the dynamic malloc/free calls the baseline was paying at every operator boundary disappear — with a secondary benefit to memory traffic, since a smaller, more compact set of reused buffers has better odds of staying warm in cache between operators. Suppose this saves 5 ms, bringing the model to 55 ms. The saving here is smaller than fusion's, which is itself a useful data point: not every optimization on this list is equally large, and knowing which ones are worth reaching for first is exactly what Section 4's hierarchy addresses.
1.5 Optimization 4: Optimized GEMM — 55 ms to 35 ms
Replacing a naive triple-nested-loop matmul with a properly blocked, register-tiled, SIMD-vectorized GEMM kernel — the subject of Lesson 4, worked through in full arithmetic detail in its GEMM and im2col companion post — is a pure compute-term attack: same FLOP count, same bytes moved in principle, but the arithmetic units spend far less time stalled waiting on memory or wasted on redundant loads, because the access pattern is now cache- and register-friendly. Suppose this is the single largest software-only win on this list: 20 ms, bringing the model to 35 ms. That it's this large is not an accident — the GEMM inside the final layer (and the GEMMs convolution reduces to, per Lesson 4's own derivation) is where the overwhelming majority of this network's FLOPs live, so a kernel-level win here has more total latency to work with than a kernel win anywhere else in the graph.
1.6 Optimization 5: NPU Offload — 35 ms to 10 ms
Moving the GEMM-heavy backbone off the CPU and onto a dedicated NPU — a fixed-function or reconfigurable MAC array purpose-built for exactly this arithmetic pattern, the subject of Lesson 10 — is the largest single jump in this entire sequence: 25 ms, bringing the model to 10 ms. This is what dedicated silicon is for: an NPU's MAC array can sustain far higher utilization on dense GEMM-shaped work than a general-purpose CPU core ever will, because every transistor in that array exists for this one job. But this optimization is not free of cost the way it might look at first glance — offloading to a separate device means the model now has to get data onto that device and back, and that cost doesn't show up in the 35-to-10 number in isolation. It shows up in the next step.
1.7 Optimization 6: Eliminating CPU/NPU Transfers — 10 ms to 7 ms
Lesson 5, Part 2 works through, in full microsecond-level arithmetic, exactly what Optimization 5 just introduced: every time execution crosses from CPU to NPU or back, the runtime pays for a memory-domain copy, frequently a layout repack (NPUs often want tiled or blocked activation layouts that CPU kernels don't produce natively), and — usually the dominant cost — a synchronization handshake between the two devices' independent execution queues. In that lesson's own worked example, two device transitions consumed 345 of 435 total microseconds against only 90 microseconds of actual useful compute — a graph that looked fast on paper lost roughly four-fifths of its wall-clock time to nothing but crossing a device boundary twice. Placing operators on the accelerator whose graph position minimizes the number of CPU/NPU transitions, and overlapping the DMA transfers that remain with ongoing compute via double buffering, closes most of that gap. Suppose this saves 3 ms here, bringing the model to a final 7 ms.
1.8 The Full Waterfall
Baseline: FP32, CPU, dynamic alloc, no fusion, generic kernels
Baseline 100.0 ms ████████████████████████████████
+ INT8 quantization (Lesson 3) 70.0 ms ██████████████████████ -30.0 ms (-30%)
+ Operator fusion (Lesson 2) 60.0 ms ████████████████████ -10.0 ms (-14%)
+ Memory planning (Lesson 5, Pt 1) 55.0 ms ██████████████████ -5.0 ms (-8%)
+ Optimized GEMM kernel (Lesson 4) 35.0 ms ███████████ -20.0 ms (-36%)
+ NPU offload (Lesson 10) 10.0 ms ███ -25.0 ms (-71%)
+ Eliminate CPU/NPU transfers (Lesson 5, Pt 2) 7.0 ms ██ -3.0 ms (-30%)
------------------------------------------------------------------------------------------------------
Cumulative: 100.0 ms → 7.0 ms ≈ 14.3x speedup (all figures illustrative)Two observations about this waterfall are worth pulling out explicitly, because they anticipate Sections 3 and 4. First, the largest wins — NPU offload and optimized GEMM, 45 of the total 93 ms saved — both came from attacking compute directly, on the operator that had the most FLOPs to begin with; the smallest win, memory planning, attacked launch overhead, which had the least room to give in the baseline breakdown. Second, and more subtly: the model's architecture never changed across these six steps. Every optimization here operated on execution strategy — precision, fusion, allocation, kernel implementation, device placement, transfer elimination — while the graph of operators the model actually computes stayed identical throughout. That is the single sentence this entire ten-lesson series has been building toward: the model barely changed; the execution strategy did, and that alone was worth roughly 14x.
2. Eight Mistakes That Undo All of It
A 14x speedup like Section 1's is achievable in principle, but it is exactly as easy to lose most of it to one avoidable error as it is to earn it through six correct ones. Each of the following mistakes has already been explained, in more depth, somewhere in Lessons 1 through 10 — this section's job is to name each one plainly, show a concrete example of it going wrong, and point at exactly which earlier lesson would have caught it before it shipped.
| # | Mistake | What Goes Wrong | Lesson That Catches It |
|---|---|---|---|
| 1 | Optimizing FLOPs only | A FLOP-count win doesn't translate to a latency win when arithmetic intensity craters alongside it | Lesson 1, Part 2 — Roofline model |
| 2 | Ignoring memory | A theoretically efficient algorithm moves too much data and becomes memory-bound regardless | Lesson 1 and Lesson 5, Part 1 |
| 3 | Optimizing kernels without profiling first | A 2x speedup on a component that's 1% of total time is invisible in the total | Lesson 9 and Lesson 10, Part 1 — Amdahl's Law |
| 4 | Assuming INT8 is always faster | Missing kernel support or dequant/requant sandwiching can make int8 slower than fp32 | Lesson 3, Part 2 |
| 5 | Ignoring data layout | A layout mismatch forces a repack or a strided access pattern that kills cache locality and vectorization | Lesson 2 and Lesson 4 |
| 6 | Ignoring copies | CPU→accelerator→CPU round trips can erase the entire benefit of the accelerator | Lesson 5, Part 2 |
| 7 | Measuring only average latency | A healthy-looking mean can hide a p99 that's five to seventy-five times worse | Lesson 9 |
| 8 | Optimizing without accuracy validation | A model that's 2x faster and 10% less accurate is often not a usable model at all | Lesson 3, Part 2 and Lesson 8 |
2.1 Mistake 1: Optimizing FLOPs Only
The trap sounds almost too naive to fall for stated abstractly — "fewer FLOPs means faster" — but it's the default assumption anyone reaches for before they've internalized the Roofline model, because FLOP counting is easy to do from an architecture diagram alone, with no profiler required. Lesson 1, Part 2 worked the counterexample in full arithmetic: a depthwise convolution over a 112×112 feature map does roughly 64 times fewer FLOPs than the equivalent regular convolution, but moves only about 1.6 times less data, because depthwise convolution never mixes channels and therefore gives each loaded byte far less work to do before it's discarded. The FLOP-count comparison alone would call the depthwise layer roughly 28 times cheaper; measured wall-clock latency on bandwidth-starved hardware tells a much less flattering story, because the operator's arithmetic intensity — FLOPs per byte moved — collapsed right alongside the FLOP count. A model architected purely to minimize a FLOP counter, without ever checking what that counter implies about arithmetic intensity, can end up no faster in practice than the FLOP-heavier model it replaced.
2.2 Mistake 2: Ignoring Memory
This is Mistake 1's general form, uncoupled from any specific operator's math: an algorithm can look efficient by every FLOP-based measure and still be memory-bound in practice, because the bottleneck was never arithmetic throughput to begin with. Lesson 2's unfused three-operator chain makes this concrete: Conv, BatchNorm, and ReLU running as three separate kernel launches move roughly 3 MB of avoidable DRAM traffic per invocation for a pair of operators — BatchNorm and ReLU — whose actual arithmetic (a multiply-add and a max, respectively) is close to free. Multiply that pattern across twenty similarly-sized layers in a real network, and the cumulative avoidable memory traffic runs into tens of megabytes per inference pass, entirely invisible to anyone reasoning about the model only in terms of its total operation count. Lesson 5, Part 1's buffer-reuse planning and Lesson 2's fusion are the two direct fixes; neither shows up if the only metric anyone tracks is FLOPs.
2.3 Mistake 3: Optimizing Individual Kernels Without Profiling First
This is the mistake that turns engineering effort itself into the wasted resource, rather than any property of the model. Suppose an engineer notices that a particular activation kernel looks unoptimized, spends a week hand-tuning it, and achieves a genuine 2x speedup on that kernel in isolation — a real, verifiable, locally correct win. If that kernel accounted for 1% of the model's total latency, Lesson 10, Part 1's Amdahl's Law framing says the best possible outcome, even with an infinitely fast version of that kernel, is a 1.01x improvement to total latency: 1 / (1 - 0.01) ≈ 1.0101. A full week of correct, well-executed engineering effort produced a result indistinguishable from noise in an end-to-end benchmark. Lesson 9's entire reason for existing is to prevent exactly this outcome — profiling first tells you which operator's fractional share of total latency is large enough that optimizing it can possibly matter, before any engineering time is spent finding out the hard way.
2.4 Mistake 4: Assuming INT8 Is Always Faster
Section 1.2's INT8 step assumed the favorable case explicitly, and Lesson 3, Part 2 spends real effort on the case where that assumption fails. Two separate failure modes stack on top of each other here. The first is missing kernel support: if the target runtime has no genuine int8 GEMM or convolution implementation for a given hardware target, a common fallback is to silently upcast the int8 operands back to float32 and run the ordinary float32 kernel underneath — which means the model now pays the cost of quantizing and dequantizing its tensors in addition to the exact same float32 compute it would have paid without ever touching int8 at all. The second, subtler failure is dequant/requant sandwiching: real graphs mix operators with mature int8 kernels (convolution, GEMM) alongside operators that often don't (certain activations, layer norm, softmax), and every time execution crosses from a supported op to an unsupported one, the runtime has to dequantize the tensor to float32, run the op, and requantize the result back — a full extra memory pass at every one of those boundaries. A model whose supported and unsupported operators interleave frequently, rather than clustering into large contiguous int8 blocks, can accumulate enough of these boundary costs to end up measurably slower than the float32 baseline it was meant to accelerate. Section 1.2's clean 30 ms saving assumed neither failure mode was present; a real quantization effort has to verify that assumption, not take it on faith.
2.5 Mistake 5: Ignoring Data Layout
Lesson 2 draws the NCHW-versus-NHWC distinction in full stride arithmetic, and Lesson 4 shows, at the level of individual cache lines, how a loop order that doesn't match a tensor's memory layout can turn a sequential, prefetch-friendly access pattern into a strided one that the hardware prefetcher can't predict. Put the two together and the failure mode is straightforward: a tensor produced in one layout (say, an NCHW activation coming out of a CPU-side convolution) fed into a kernel that expects the other (an NHWC-optimized accelerator kernel) forces either an explicit transpose — a full extra memory pass, functionally identical in cost to the dequant/requant boundaries in Mistake 4 — or, worse, a kernel that silently tolerates the mismatched layout by falling back to a scalar, non-vectorized access pattern with none of the register-blocking or SIMD benefits Lesson 4 spent an entire lesson deriving. Either way, the operator's real-world latency has nothing to do with how well-optimized its kernel looks in isolation, and everything to do with whether the tensor handed to it was already in the layout that kernel was written for.
2.6 Mistake 6: Ignoring Copies
Section 1.7's NPU-transfer-elimination step is this mistake's fix, worked through in reverse: Lesson 5, Part 2's own device-placement example put two convolutions and one CPU operator through a CPU→NPU→CPU round trip and found that of 435 total microseconds, only 90 — about a fifth — were actual useful compute, with the remaining four-fifths consumed entirely by the mechanics of crossing a device boundary twice: memory-domain copies, layout repacks, and above all synchronization handshakes between the two devices' independent execution queues. The lesson generalizes past this one specific NPU/CPU example: any device-to-device round trip, whether it's CPU-to-GPU-to-CPU in a training pipeline or a poorly-placed operator bouncing a tensor back and forth in an inference graph, can silently erase the entire benefit the accelerator was brought in to provide. A model that spends more of its wall-clock time shuttling data across a device boundary than it spends computing on either side of that boundary hasn't actually been accelerated — it's been relocated, at a cost.
2.7 Mistake 7: Measuring Only Average Latency
Lesson 9 makes the case with a single distribution that a mean number can hide entirely: a system with median and p95 latency both sitting at a comfortable 8 ms can, at the same time, have a p99 of 40 ms — five times the median — and a p99.9 of 600 ms, seventy-five times the median, none of which a dashboard reporting a single "average latency" figure of roughly 12 ms would reveal, since that average looks close enough to the 8 ms steady-state baseline to wave off as ordinary noise. Dean and Barroso's "Tail at Scale" argument, which that lesson leans on directly, is what makes this more than a statistics curiosity for a production inference service specifically: at real request volumes, "rare" tail events aren't rare from the system's point of view, and if a single user-facing request depends on more than one backend inference call, the probability that the whole request avoids the slow tail on every single call compounds fast enough that tail latency at the component level becomes ordinary, everyday latency at the aggregate level. A benchmark that reports only a mean cannot distinguish "this system is fast for everyone" from "this system is fast for 97% of requests and unacceptable for the rest" — and for anyone actually operating the service, that's usually the entire question that matters.
2.8 Mistake 8: Optimizing Without Accuracy Validation
Every technique in this series that trades precision or capacity for speed — quantization's coarser numeric grid, Lesson 8's pruning and distillation — buys that speed by discarding some information the model previously used, and none of them guarantee in advance exactly how much accuracy that costs. Lesson 3, Part 2's calibration section shows one concrete way this goes wrong: a calibration range chosen without accounting for outliers, or chosen too generously to avoid clipping a single rare spike, can leave the bulk of a tensor's ordinary values compressed into a much coarser slice of the int8 grid than necessary, quietly degrading accuracy in a way that never shows up in a latency benchmark at all. The same risk applies to structured pruning at too aggressive a ratio, or to a distilled student trained against too small or too narrow a teacher signal — Lesson 8 is explicit that structured pruning's coarse, all-or-nothing removal decisions are, weight-for-weight, more damaging to accuracy than unstructured pruning's fine-grained selectivity, precisely the tradeoff that makes structured pruning's guaranteed speedup non-free. A model that got 2x faster and lost 10% of its accuracy along the way is not a strictly better model than the one it replaced — it's a different model, on a different point of the speed-accuracy curve, and whether that new point is acceptable is a question a latency number alone can never answer. Every optimization in Section 1's waterfall needs its own accuracy check run alongside its own latency measurement, not after the fact and not instead of it.
3. The Performance Equation
Section 1.1 introduced a five-term breakdown of the baseline model's 100 ms without much justification for the split. Here is the justification: a useful mental model for any inference workload's total latency is
T_total = T_compute + T_memory + T_launch + T_sync + T_transferThis is not a physical law — the five terms can overlap in time when execution is well-pipelined (that overlap is exactly what Section 1.7's DMA double buffering exploits), so a naive sum can overstate the true wall-clock total on well-optimized systems. What the equation is genuinely useful for is diagnosis: given a profiler's breakdown, it turns "the model is slow" into "the model is slow because of this specific term," and every term maps onto a specific, already-covered set of techniques for reducing it.
| Term | What It Is | What Drives It Up | Lesson(s) That Reduce It |
|---|---|---|---|
T_compute | Time the arithmetic units spend executing the model's FLOPs | Unvectorized kernels, poor cache/register blocking, unnecessarily high precision | Lesson 4 (kernel engineering), with Lesson 1 diagnosing whether compute is even the bottleneck and Lesson 3 cutting cost-per-op |
T_memory | Time spent moving tensor data through the memory hierarchy (DRAM, cache, on-chip SRAM) | Unfused operator chains, poor data layout, redundant reads/writes of intermediates | Lesson 1 (memory hierarchy and arithmetic intensity) and Lesson 5, Part 1 (buffer reuse, arena allocation) |
T_launch | Fixed per-operator overhead: kernel dispatch, dynamic allocation, graph interpretation | Many small operators, dynamic (non-arena) memory allocation, an unfused graph | Lesson 5, Part 2 (graph execution, batching launches), with Lesson 2's fusion reducing the operator count launch overhead scales with |
T_sync | Time spent waiting on synchronization — between CPU and accelerator, or between independent execution queues | Poor overlap between compute and data movement, unnecessary handshakes at every device boundary | Lesson 5, Part 2 (CPU/NPU overlap, double-buffered DMA, asynchronous execution) |
T_transfer | Time spent physically moving data across a memory-domain or device boundary | Frequent device transitions, layout repacks at the boundary, no zero-copy path | Lesson 5, Part 2 (operator placement, zero-copy, DMA, unified/shared memory) |
3.1 Reading the Baseline Through This Lens
Return to Section 1.1's breakdown: 55 ms compute, 30 ms memory, 10 ms launch, 3 ms sync, 2 ms transfer. That distribution is itself diagnostic — more than half the model's time is compute, which is exactly why Sections 1.5 and 1.6 (optimized GEMM and NPU offload, the two largest wins in the whole waterfall) targeted T_compute directly, and why the equation predicts those two steps had the most room to give. Memory was the second-largest term, and Sections 1.3 and 1.4 (fusion and memory planning) targeted it, together accounting for the third- and sixth-largest wins. Launch, sync, and transfer started small in the baseline — 15 ms combined, versus 85 ms of compute and memory — which is consistent with Section 1's later steps producing smaller absolute savings even as they were individually well-executed: there simply wasn't as much room in those three terms to begin with. Note the one exception the equation predicts and the waterfall confirms: T_sync and T_transfer actually grew between Sections 1.5 and 1.6, because moving compute onto the NPU introduced device-crossing costs the CPU-only baseline never had to pay — which is exactly why Section 1.7 exists as its own step rather than being folded into Section 1.6.
3.2 Why the Equation Is a Diagnostic Tool, Not a Universal Formula
The five-term split is deliberately coarse. It doesn't distinguish L1 cache misses from DRAM bandwidth saturation within T_memory, and it doesn't distinguish a queue-depth stall from a semaphore wait within T_sync — for that level of resolution, Lesson 9's actual profiling tools (hardware performance counters, timeline traces) are the right instrument, not this equation. What this equation is for is the first, cheapest diagnostic pass: before reaching for a profiler at all, a rough mental estimate of which term dominates a given workload usually points at which lesson's toolkit is worth reaching for first — a rare instance where naming the categories correctly does real intellectual work before a single measurement has been taken.
4. The Hierarchy of Optimization: What Order to Actually Attack This In
Section 1's six optimizations were presented in the order the source material happened to list them, and that order is close to, but not identical to, the order a practitioner starting from scratch should actually work in. This section argues for that order directly, rather than just listing it.
4.1 The Tree
4.2 Why the Top Dominates: An Amdahl Argument
Mistake 3 already introduced the mechanism; the hierarchy is what happens when that same mechanism is applied one level up, comparing architecture-level decisions against kernel-level ones rather than comparing two kernels against each other. Lesson 10, Part 1's Amdahl's Law framing says that if a component accounts for fraction p of total latency, no optimization confined to that component — no matter how aggressive — can improve total latency by more than a factor of 1 / (1 - p). Suppose an attention block accounts for 40% of a transformer's total inference latency (p = 0.4). Kernel-level tuning of the dense-attention GEMMs inside that block — better blocking, better vectorization, everything Lesson 4 covers — is capped, even in the fantastical limit of an infinitely fast kernel, at 1 / (1 - 0.4) = 1.67x total speedup, because the other 60% of the model's latency was never touched.
Now compare that against an architecture change: replacing dense attention with a sparse-attention variant that does the same job with, say, 8 times fewer FLOPs in that block. This isn't a kernel optimization at all — it's a decision made one level up the hierarchy, before any kernel gets written — and its ceiling is completely different, because it doesn't just speed up the 40% of latency the attention block already occupies, it shrinks that 40% down toward roughly 40%/8 ≈ 5% of the original total. A model that used to spend 40% of its time on attention now spends roughly 5%, freeing up nearly all of the remaining 35 percentage points for the rest of the model to occupy a larger relative share — and critically, once that architecture-level shrinkage has happened, Lesson 4's kernel tuning can still be applied on top, now operating on a much smaller base. Fixing the algorithm first doesn't compete with kernel tuning; it changes what kernel tuning has left to work with, and Amdahl's ceiling for the next optimization gets recomputed against the smaller number every time a higher level of the hierarchy moves first.
4.3 Walking Down the Hierarchy
Each level below Architecture follows the same logic at a smaller scale, and each one's ceiling is set by how much of the model's total latency the level above it left on the table:
- Architecture and Compression set the shape of the computation graph itself — which operators exist at all, and how many parameters and FLOPs each one costs. Lesson 8's point that "removing an operation entirely is better than optimizing its kernel" belongs here: a pruned or distilled model doesn't need its removed structures optimized, because they no longer exist to be optimized.
- Graph-level fusion (Lesson 2) operates on the architecture decided above it — it can't invent new sparsity or remove an operator the architecture requires, but it can eliminate the memory round trips between whatever operators remain, which is why it comes before precision: fusing first means precision-conversion ops (Section 4.4's next level) have fewer boundaries to insert themselves at.
- Precision (Lesson 3) changes the cost of each remaining operator without changing which operators exist or how they're connected — a multiplicative discount applied after the graph shape is fixed, which is exactly why Mistake 4's dequant/requant sandwiching is a graph-level problem in disguise: quantizing before fusion decisions are settled is how those sandwiches end up scattered through the model in the first place.
- Memory planning (Lesson 5, Part 1) and the underlying memory hierarchy reasoning from Lesson 1 determine how efficiently the now-fused, now-quantized graph's tensors move through DRAM, cache, and on-chip memory — a real win, but one bounded by how much traffic the graph and precision decisions above it left to plan around.
- Kernels (Lesson 4) tune the execution of individual operators that everything above has already decided must exist, in the precision already decided, moving data through the memory plan already decided — which is precisely why Section 4.2's Amdahl argument bounds kernel tuning's ceiling by the operator's fractional share of a total that four higher levels have already shaped.
- Runtime (Lesson 5, Part 2) schedules and places the kernels that now exist, minimizing launch overhead, synchronization stalls, and transfer costs between devices — squeezing the overhead around computation that every level above has already fixed in shape.
- Hardware (Lesson 10) is the foundation everything above ultimately targets — an NPU offload decision only pays off in proportion to how well the graph, precision, memory plan, kernels, and runtime feeding it were already tuned to hand it well-shaped, well-scheduled work.
4.4 What This Doesn't Mean
This hierarchy is a prior about where the largest wins usually live, not a rule that a real project must literally execute top-to-bottom on every model. Two qualifications matter. First, Lesson 9 and Mistake 3 still govern: the hierarchy says where wins tend to be largest across models in general, but a specific model's actual profile — not the tree — is what should decide where a specific project starts, because a model that's already architecturally efficient and precision-optimized may genuinely have its largest remaining win sitting in the Runtime or Hardware layers, exactly where Section 1.6 and 1.7 found the largest and third-largest wins in this post's own worked example. Second, the levels aren't strictly independent — Section 3.1 already showed T_sync and T_transfer growing when Section 1.6's hardware-level NPU offload was applied, which is Section 4.3's Runtime layer reacting to a decision made at the Hardware layer above it. The hierarchy predicts where to look first; it doesn't excuse skipping the measurement that confirms whether that's actually where this particular model's time is going.
Further Reading
- NVIDIA, "Best Practices — NVIDIA TensorRT Documentation" — the official vendor guidance on measuring before optimizing, precision selection, and profiling methodology referenced throughout Section 2.
- PyTorch, "Model Inference Optimization Checklist", PyTorch/Serve documentation — a widely used, official practitioner checklist covering precision, batching, and system-level bottlenecks, grounding this post's own mistakes list.
- Jeffrey Dean and Luiz André Barroso, "The Tail at Scale", Communications of the ACM (2013) — the foundational treatment of tail latency underlying Mistake 7's argument.
- Amdahl, G. M., "Validity of the Single Processor Approach to Achieving Large Scale Computing Capabilities," AFIPS Spring Joint Computer Conference, 1967 — the original paper behind the Amdahl's Law argument in Sections 2.3 and 4.2. PDF via Stony Brook CSE613 course archive
- Krishnamoorthi, R., "Quantizing deep convolutional networks for efficient inference: A whitepaper" (2018) — covers the calibration and accuracy-validation considerations underlying Mistakes 4 and 8.
This lesson stacked techniques and cataloged failure modes; it did not yet say how to think about the whole problem at once, where to start learning it, or what to actually build to prove any of it out. Lesson 11, Part 2 — "The Complete Mental Model, Learning Path, and Projects" — closes the series with exactly that.