Exercises — Model serving (REST APIs, FastAPI)
The two formulas we lean on come straight from the parent note:
Before we start, a one-time reminder of the unit trick we use constantly: milliseconds → seconds means divide by , because . So .
Level 1 — Recognition
Goal: can you name the piece and pick the right label? No maths yet.
L1.1 — For each row of a POST /predict conversation, name the piece (Endpoint, Request body,
Response, or Status code):
(a) /predict (b) {"x1": 2.0, "x2": 5.1} (c) 422 (d) {"pred": 1, "prob": 0.87}
Recall Solution L1.1
(a) Endpoint — it is the URL path the client hits. (b) Request body — the input features, sent as JSON. (c) Status code — the outcome signal ( = Unprocessable Entity). (d) Response — the prediction returned as JSON.
L1.2 — Match each tool to its ONE job: FastAPI, Uvicorn, Pydantic, joblib. Jobs: (i) owns the socket and speaks HTTP; (ii) validates/parses incoming JSON; (iii) binds URL+method to a Python function; (iv) loads the model file from disk.
Recall Solution L1.2
- Uvicorn ::: (i) owns the socket and speaks HTTP — the mail carrier.
- Pydantic ::: (ii) validates/parses incoming JSON — the note-checker.
- FastAPI ::: (iii) binds URL+method to a Python function via
@app.post(...). - joblib ::: (iv) loads the model file (
model.pkl) from disk.
Level 2 — Application
Goal: plug numbers into the two formulas.
L2.1 — A server has Uvicorn workers. Each handles one request in . Compute the max throughput in req/s.
Recall Solution L2.1
WHAT: convert to seconds, then apply throughput . WHY: the formula needs in seconds so the answer comes out in req/s.
L2.2 — A single request breaks down as: , , , , (all ms). Find and the throughput of one worker.
Recall Solution L2.2
WHAT: sum the pipeline stages (they happen one after another for a single request). One worker: req/s.
L2.3 — With the L2.2 breakdown, which single stage should you optimize first, and by the 80/20 rule roughly how much of total time is it?
Recall Solution L2.3
WHAT: find the dominant term. ms is the biggest. WHY: optimizing a chunk pays far more than shaving the ms deserialize step. Attack inference first (batching, quantization, a smaller model). See Batching & Inference Optimization.
Level 3 — Analysis
Goal: compare designs and explain the trade-off, not just compute.
L3.1 — Design A: one worker, ms/request. Design B: batching — one call on rows takes ms total. Compare throughput (rows/s) and per-row latency.
Recall Solution L3.1
WHAT/WHY: batching amortizes fixed overhead across many rows, but each individual row now waits for the whole batch.
- Design A throughput: rows/s. Per-row latency: ms.
- Design B throughput: rows in s rows/s. Per-row effective latency: ms of compute, but a row may wait up to ms for the batch to fill. Trade-off: batching multiplies throughput (, a gain) at the cost of tail latency. Great for offline scoring, risky for a real-time UI.
Look at the figure: the flat batch bar towers over the single-request bar for throughput, while the right panel shows latency moving the opposite way.

L3.2 — You need req/s. Each worker does ms/request. How many workers minimum?
Recall Solution L3.2
WHAT: invert the throughput formula for . Since , WHY exactly 15, not 14.x: workers give ; give . You must round up — you can't run a fractional worker. See Load Balancing & Autoscaling.
L3.3 — Client sends {"x1": "cat", "x2": 5.1}. Compare the outcome with vs without the
Pydantic schema, and say which HTTP status class each returns and who it blames.
Recall Solution L3.3
- Without Pydantic: the string
"cat"reachesmodel.predict, which throws aValueError. FastAPI turns an uncaught exception into500— a 5xx, which blames the server. - With Pydantic: validation fails before the model runs →
422Unprocessable Entity, a 4xx, correctly blaming the client's bad input. Analysis: the same bad request produces opposite fault attribution.4xx= "you sent garbage";5xx= "I broke." Pydantic shifts the blame to the correct party and protects the model.
Level 4 — Synthesis
Goal: assemble multiple pieces into a working design and reason about it end-to-end.
L4.1 — Sketch the request lifecycle for POST /predict as an ordered flow, naming which
component owns each step, and give the mnemonic from the parent note.
Recall Solution L4.1
Order (L-V-P-R): Listen → Validate → Predict → Return.
Mnemonic: "Little Vans Please Reverse" = Listen, Validate, Predict, Return. Key design choice: the model is loaded once at startup and stays in RAM, so step "M" never touches disk. See Docker & Containerization for shipping this whole stack as one image.
L4.2 — A model needs a GPU and is GB on disk. You must serve a JavaScript web frontend. Justify (a) using a REST API instead of importing the model in JS, and (b) loading at startup, and (c) computing the throughput if a request costs ms with replicas.
Recall Solution L4.2
(a) A JS frontend cannot pickle.load a PyTorch model and has no GPU — but it can send an HTTP
request. REST gives language independence and one source of truth for the GB weights,
instead of copying them everywhere.
(b) Loading a GB model from disk is expensive; doing it per request would add hundreds of ms
and could exhaust memory. Load once at module/startup scope so all requests reuse the RAM copy.
(c) , :
Level 5 — Mastery
Goal: multi-step reasoning, degenerate cases, and pushing formulas to their limits.
L5.1 — You currently serve at ms (, rest ). You cut inference in half to ms via quantization. (a) New ? (b) New single-worker throughput? (c) By what factor did throughput improve — and why is it NOT ?
Recall Solution L5.1
(a) (the non-inference ms is untouched — Amdahl's law in disguise). (b) req/s. (c) Old throughput req/s, so improvement , not . WHY: you only halved the inference half of the pipeline. The fixed ms floor caps how much any inference speedup can help — the classic diminishing-returns wall.
L5.2 — Degenerate limit. Suppose you make inference free: . With the ms of non-inference overhead fixed, what is the maximum possible single-worker throughput? What does this tell you about where to look next?
Recall Solution L5.2
As , , so Even with a perfect, instant model you cannot beat req/s per worker. The ceiling is set by . Next optimization must target those (bigger batches, faster serialization / gRPC, less preprocessing), not the model. This is why you always find the dominant term after each fix.
L5.3 — Batch vs many-calls, full comparison. Scoring rows two ways:
Plan A — separate POST /predict calls, each ms on one worker.
Plan B — one POST /predict_batch of rows: fixed overhead ms + ms per row of
inference. Compute total wall-clock time for each and the speedup.
Recall Solution L5.3
Plan A: (each call pays the full
overhead again).
Plan B: overhead paid once + vectorized inference:
Speedup: .
WHY so huge: Plan A repeats the fixed per-request overhead times; Plan B amortizes it
once and lets the model's vectorized predict chew all rows together. This is the core argument
for the batch endpoint in the parent note (Example 3). See Batching & Inference Optimization.
Wrap-up recall
Recall One-line takeaways (hide and recite)
- Throughput uses in seconds ::: convert ms → s by dividing by 1000 first.
- To hit a target rate, workers ::: then round up.
- Speeding one pipeline stage helps only by its share ::: fixed overhead sets a ceiling.
- Batching trades tail latency for huge throughput ::: amortize overhead across rows.
4xxblames the client,5xxblames the server ::: Pydantic converts crashes into422.