5.3.6 · D4MLOps & Deployment

Exercises — Model serving (REST APIs, FastAPI)

2,431 words11 min readBack to topic

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.

Figure — Model serving (REST APIs, FastAPI)

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" reaches model.predict, which throws a ValueError. FastAPI turns an uncaught exception into 500 — a 5xx, which blames the server.
  • With Pydantic: validation fails before the model runs → 422 Unprocessable 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.

POST predict

Client

Uvicorn owns socket

FastAPI routes request

Pydantic validates JSON

Model predict in RAM

Return dict as JSON 200

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.2Degenerate 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.3Batch 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.
  • 4xx blames the client, 5xx blames the server ::: Pydantic converts crashes into 422.