Skip to content

Phase 8 - Scaling & Deploying Deep Models

Every phase before this one ended with a trained model sitting in a notebook. This phase is about the distance between that and something other people can use — and about how much of the standard advice for crossing it is hardware-specific, already applied for you, or measurably wrong on the machine in front of you.

Everything below was measured on this machine: TensorFlow 2.21, CPU only, 8 cores, no GPU. No keras_tuner, no TensorFlow Serving binary. Those absences turned out to be useful — they forced each page to measure the underlying mechanism rather than demonstrate an API.

PageThe claim being testedResult
tf.dataAdd num_parallel_calls for a big winAlready applied — tf.data rewrites a stateless map for you (3.53× of the 3.93×)
Custom loopsHand-written loops are slower and riskierMatched fit to 0.0010; forgetting @tf.function costs 9.9× per step
tf.distributeDistribution is nearly free1 replica costs 1.50× before parallelising anything; updates halve 282 → 141
Hyperparameter tuningSearch strategies differ a lotHalving found the exact best in 64 epochs; exhaustive needed 144
Mixed precisionOne line, roughly 2× faster40.65× SLOWER here — it is a GPU feature, not a model feature
TF ServingServing is a packaging stepA client preprocessing differently dropped accuracy 0.9470 → 0.0895, silently
TensorFlow LiteQuantisation shrinks modelsTrue (11.54×) — but conversion alone gave 3.06× and 77× lower latency
Model compressionCompression costs accuracyPruning 50% improved it (0.9773 vs 0.9733); 95% destroyed it
LimitationsDeep learning generalisesMeasured against rotation, shift, contrast and adversarial noise
figure Identical model and batch, 8-core CPU, no GPU matplotlib
Left: bars of milliseconds per step for the three policies - 18.92 for float32, 769.21 for mixed_float16 and 67.50 for float64, each annotated with its ratio. Right: validation accuracy per epoch for the three policies, three nearly overlapping curves ending at 0.9073, 0.9067 and 0.9173. Left: bars of milliseconds per step for the three policies - 18.92 for float32, 769.21 for mixed_float16 and 67.50 for float64, each annotated with its ratio. Right: validation accuracy per epoch for the three policies, three nearly overlapping curves ending at 0.9073, 0.9067 and 0.9173.
The right panel is the control: accuracy is essentially unaffected - 0.9073 against 0.9067 - so the float16 arithmetic is numerically adequate for this model with loss scaling in place. Only the speed claim fails. float64 is included as a reference point for what genuinely-more-work looks like: 3.57x, far more reasonable than the conversion overhead of a dtype the hardware cannot execute.
figure 300 samples, batch of 1, through the TFLite interpreter matplotlib
Left: horizontal bars of milliseconds per sample, with Keras at 1.627 far above the four TFLite variants between 0.021 and 0.033. Right: a scatter of accuracy against size on a log axis, showing all five variants at essentially the same accuracy across a 12x range of sizes. Left: horizontal bars of milliseconds per sample, with Keras at 1.627 far above the four TFLite variants between 0.021 and 0.033. Right: a scatter of accuracy against size on a log axis, showing all five variants at essentially the same accuracy across a 12x range of sizes.
Every TFLite variant is between 49x and 77x faster than Keras at batch size 1, and the differences among them are small - 0.021 ms for dynamic-range against 0.033 ms for float32. The right-hand panel is the summary of the whole page: a flat accuracy line across a 12x span of model sizes, which is why quantisation is close to free on a task like this.
figure Every claim this phase set out to test matplotlib
Horizontal bars, one per page in the phase, each labelled with the claim it tested and coloured by the verdict: green where the standard story held, amber where it held at a price, red where the measurement contradicted it. 5 of 6 claims contradicted, 1 held at a price, 0 held. Horizontal bars, one per page in the phase, each labelled with the claim it tested and coloured by the verdict: green where the standard story held, amber where it held at a price, red where the measurement contradicted it. 5 of 6 claims contradicted, 1 held at a price, 0 held.
Collected from the runs behind each page's own figures rather than measured afresh, so every bar is traceable to the page it names. Bar length is the log of the effect size, because the effects span from 0.0014 to 5,376 — the number that matters is printed on each bar. Across all nine phases, 34 of 54 claims were contradicted outright, 11 held at a cost that was worth stating, and 9 held as advertised.
diagram Diagram mermaid

Three lessons recur across the nine pages, and they are worth stating separately from the numbers.

Most performance advice is hardware advice in disguise. Mixed precision is the clearest case — the same line that gives roughly 2× on tensor cores gave 40.65× slower here. The same is true of distribution (1.50× overhead with nothing to parallelise onto) and of float16 more generally. Measure one epoch on your own hardware before adopting any of it.

Modern frameworks already applied the tutorial. tf.data rewrote a stateless map into a parallel one before being asked, so the classic “add num_parallel_calls” benchmark measures almost nothing on default settings. Finding out what an option is worth now requires switching the automatic rewrites off.

Deployment failures do not raise exceptions. A client that preprocesses differently from training returns a perfectly-shaped probability vector at 8.5% accuracy and 0.81 confidence. Quantisation changes 4 predictions in 2,000 without moving accuracy. Fine-tuning a pruned model without re-applying the mask keeps the accuracy and silently discards the sparsity. Every one of these is invisible unless something external is measuring it.

sketch How often the standard story survived p5.js
Step through the phases. Each bar splits the claims that phase tested into contradicted, held at a price, and held as advertised - the totals are summed live.

Two numbers that changed how the rest of the phase was built

Section titled “Two numbers that changed how the rest of the phase was built”

model.predict() costs about 470 ms per call in a tight loop, an eager model(x) call about 28 ms, and a traced tf.function about 1.2 ms. That 390× spread decided whether several pages in this module — and the whole reinforcement-learning phase — were runnable at all.

And per-sample inference cost falls 14× from batch 1 to batch 128. Framework overhead, not arithmetic, dominates small-batch work; that single fact explains the TFLite latency result, the serving batching scheduler, and why predict() is the wrong tool inside a loop.

sketch What one forward pass costs p5.js
Pick a way to run a batch through a model. The bar is the measured time per call on this machine, on a log scale because the spread is 390x.
  1. tf.data — get data to the model faster than it consumes it.
  2. Custom models and training loops — replace fit when you need to, and pay the tracing cost knowingly.
  3. Distributed training — what a strategy does to your batch, your update count and your learning rate.
  4. Hyperparameter tuning — spend a fixed budget well.
  5. Mixed precision — the clearest example of hardware-dependent advice.
  6. TensorFlow Serving — the SavedModel contract and the skew failure it prevents.
  7. TensorFlow Lite — conversion, quantisation, and where the size actually goes.
  8. Model compression — pruning and distillation, measured against the same baseline.
  9. Limitations — what none of this fixes.
  • Real multi-GPU or TPU results. There are no accelerators here, so the pages measure semantics and overhead and say so.
  • Production infrastructure. Load balancers, autoscaling, canary deployments and feature stores are all downstream of the model artefact this phase produces.
  • Very large models. Everything here is MNIST-sized; sharding a model that does not fit on one device is a different problem from replicating one that does.

Each page states its own budget and hardware beside its numbers, so nothing needs to be taken on trust.

Start with Loading & Preprocessing Data with tf.data — the pipeline that feeds everything else, and the page that shows how much of its own advice the framework has already taken.

pch.quizTag pch.quizDefaultTitle
  1. `mixed_float16` measured 769.21 ms per step against float32's 18.92 ms - 40.65x slower - while final accuracy moved only 0.9073 to 0.9067. What does the accuracy control establish?

    pch.quizShowAnswer

    B — That the float16 arithmetic is fine and only the speed claim failed - this is an 8-core CPU with no tensor cores, so the dtype conversions are pure overhead

  2. A client that preprocessed differently from training dropped accuracy from 0.9470 to 0.0895 and raised no error. What class of failure is that?

    pch.quizShowAnswer

    B — A serving skew: the SavedModel contract covers tensor shape and dtype but not the meaning of the numbers, so a wrong-but-well-formed input returns a confident wrong answer

  3. Adding `num_parallel_calls` to a stateless `map` gave only 3.93x total, of which tf.data's own automatic rewrite already supplied 3.53x. What follows for benchmarking framework advice?

    pch.quizShowAnswer

    B — You have to turn the automatic optimisations OFF to measure what an option is worth - otherwise you are measuring a rewrite the framework already applied

  4. Pruning 50% of the weights IMPROVED accuracy (0.9773 against 0.9733) while pruning 95% destroyed it. What is the honest way to report this?

    pch.quizShowAnswer

    C — Moderate pruning acted as regularisation on this model and dataset, and the useful deliverable is the whole sparsity curve rather than any single point on it

  5. Per-sample inference cost fell 14x from batch 1 to batch 128. Which later result does that single fact explain?

    pch.quizShowAnswer

    B — Why TFLite is 77x faster at batch 1, why serving needs a batching scheduler, and why `predict()` is the wrong call inside a loop

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading