DSC3103 ยท Module 7 of 9

From Model to Data Product: FastAPI & Docker

Serve ML models with zero training-serving skew (Lab 9 Part 2 / Lab 10)

~56 min to study properly

A trained scikit-learn model sitting in a Jupyter notebook is not a data product. Downstream applications need a reliable HTTP API that accepts a JSON request, validates it, and returns a prediction in milliseconds.

Checkpoint ยท the examiner asks

What is training-serving skew in ML systems? Explain how a subtle threshold mismatch causes silent prediction errors, and how shared feature modules, Pydantic schemas, and Docker work together to make a model service reproducible.

In one minute

A model becomes a data product when it sits behind a validated HTTP API. Save the model with its metadata (version, feature columns, metrics), load it once at startup rather than per request, validate inputs with strict Pydantic schemas (HTTP 422 for bad payloads), and share feature code between training and serving to prevent training-serving skew. Package it with pinned dependencies in Docker.

Traps examiners look for
  • Loading model.joblib inside the /predict handler.
  • Re-implementing feature logic in the API (training-serving skew).
  • Unpinned requirements so the container differs from training.
Read

Study notes

01What makes a model a data product

A model in a notebook answers questions only for the person running the notebook. A data product answers them for other systems, reliably: an HTTP endpoint that accepts a JSON request, validates it, returns a prediction in milliseconds, logs what happened, and behaves the same tomorrow as today.

The pieces from Lab 9 Part 2 / Assignment 04:

  • models/model.joblib โ€” the serialised model (or pipeline).
  • models/model_metadata.json โ€” model_version, feature_columns, training date, metrics. The API reports the version so every prediction can be traced to a model.
  • src/common/features.py โ€” the one place features are computed.
  • src/api/main.py โ€” FastAPI app with a Pydantic request schema and a /predict endpoint.
  • A Dockerfile with pinned requirements.txt, so the runtime is identical everywhere.

02Load once, at startup

Loading a model from disk takes time โ€” tens to hundreds of milliseconds for a medium RandomForest. Predicting one row takes a few milliseconds. If /predict calls joblib.load() every time, almost all of every request is spent re-reading the same file.

Load it once when the app starts (module import or FastAPI lifespan) and keep it in memory:

@asynccontextmanager
async def lifespan(app):
    app.state.model = joblib.load("models/model.joblib")
    yield

Total time for N requests: per-request loading = N ร— (load + infer); startup loading = load + N ร— infer. With load = 150 ms, infer = 5 ms and 200 requests, that's 31 s vs 1.15 s.

03Training-serving skew

Training-serving skew is when the features the model sees in production are computed differently from the features it was trained on. Nothing crashes โ€” the API returns HTTP 200 with a perfectly valid number โ€” but the number is wrong.

In Lab 9.2 Step 8, training defined is_rainy = rainfall_mm > 5.0, but a copy of the logic in the API used > 10.0. For rainfall_mm = 8.2, training says rainy, the API says not rainy, and the beans prediction silently moves from 2,927.25 to 2,889.58 UGX.

The fix is architectural, not "be more careful": put make_features() and FEATURE_COLUMNS in one shared module imported by both train.py and the API, save the feature list in the model metadata, and check at startup that the request schema produces exactly those columns in that order.

04Validation with Pydantic and packaging with Docker

A Pydantic model turns the API's input contract into code:

class PriceRequest(BaseModel):
    market: Literal["Owino", "Nakasero", "Kalerwe", "Gulu"]
    commodity: Literal["Beans", "Maize", "Matooke", "Rice"]
    rainfall_mm: float = Field(ge=0, le=500)
    date: datetime.date

A bad payload โ€” a negative rainfall, an unknown market, a missing field โ€” gets an automatic HTTP 422 with a message saying exactly which field is wrong, before any model code runs.

Docker then freezes everything else: the Python version, the exact library versions (scikit-learn==1.5.2, not "whatever pip installs today" โ€” a pickled model can break across versions), the code and the model file. Ordering the Dockerfile as copy requirements โ†’ pip install โ†’ copy code lets Docker cache the dependency layer, so a code change rebuilds in seconds.

Watch it done

Worked examples

Worked example 1

Is my API skewed?

Training used is_rainy = rainfall_mm > 5.0; the API uses > 10.0. Requests arrive with rainfall 2.0, 7.5, 9.9 and 14.0 mm. Which get a wrong prediction?

    Worked example 2

    Startup loading pays for itself

    Model load = 220 ms, predict = 6 ms. How many seconds does loading at startup save over 500 requests, and what's the speed-up?

      Vocabulary

      Key terms

      Hidden Technical Debt in Machine Learning Systems

      Google's famous NeurIPS paper ('Hidden Technical Debt in ML Systems', Sculley et al.) showed that the ML model code itself is often less than 5% of a production ML system โ€” the other 95% is data verification, feature extraction, serving infrastructure, and monitoring.

      Practise

      Quick check

      0 of 8 correct so far. Wrong answers come with an explanation โ€” and the buddy can unpack them.

      Q1In a Dockerfile, why should `COPY requirements.txt .` and `RUN pip install -r requirements.txt` come BEFORE `COPY src/ ./src/`?

      Q2Loading `model.joblib` from disk takes 180 ms and `model.predict()` takes 4 ms. For 100 sequential requests, how much total time is saved by loading the model ONCE at startup instead of on every request?

      Q3Where should make_features() live?

      Q4An API receives rainfall_mm = -4. With a Pydantic Field(ge=0), what happens?

      Q5Why store model_version in model_metadata.json and return it from the API?

      Q6Training-serving skew usually shows up asโ€ฆ

      Q7Which Dockerfile order keeps rebuilds fast?

      Q8Why pin scikit-learn in requirements.txt?

      Exam-style

      Written practice

      Short answer6 marks

      What is training-serving skew? Use the Lab 9.2 Step 8 example and give the architectural fix.

      Short answer5 marks

      Why should the model be loaded at startup and not inside the /predict handler? Quantify with load = 150 ms, predict = 5 ms and 200 requests.

      Long answer12 marks

      Design the FastAPI + Docker service for the market-price model. Describe the project layout, the request schema with at least three validation rules, how you prevent training-serving skew, how the model is loaded and versioned, and how the Dockerfile keeps builds reproducible and fast.

      Watch

      Videos

      Fresh picks are chosen by AI every day.
      Hands-on

      FastAPI Model Serving & Training-Serving Skew Lab

      4 guided tasks: predict, experiment, explain โ€” then challenge yourself. Plus 2 coding exercises you run right in the browser.

      Start the lab