Level 5 — MasteryMLOps & Deployment

MLOps & Deployment

90 minutes60 marksprintable — key stays hidden on paper

Time limit: 90 minutes Total marks: 60 Instructions: Answer all questions. Show all derivations, code, and reasoning. Numeric answers must be justified. Partial credit is awarded for correct method.


Question 1 — Inference Serving: Latency, Throughput & Cost (24 marks)

You are deploying a quantized LLM behind a real-time REST API (FastAPI + a vLLM backend). The system must satisfy a p99 latency SLO of 500 ms at a target load.

Assume the serving node processes requests via continuous batching. Each request's total service time is modelled as:

Treq=Tfixed+Loutr(b)T_{\text{req}} = T_{\text{fixed}} + \frac{L_{\text{out}}}{r(b)}

where Tfixed=40T_{\text{fixed}} = 40 ms (prefill + network), Lout=256L_{\text{out}} = 256 output tokens, and the per-request decode rate under batch size bb is

r(b)=Rmaxb,Rmax=8000 tokens/s (aggregate GPU decode throughput).r(b) = \frac{R_{\max}}{b}, \qquad R_{\max} = 8000 \text{ tokens/s (aggregate GPU decode throughput)}.

(a) Derive TreqT_{\text{req}} as a function of bb and find the largest batch size bb^\star that still meets the 500 ms latency bound (assume every request in the batch decodes concurrently). (5)

(b) The arrival process is Poisson with rate λ\lambda requests/s. Model one serving replica as an M/M/1 queue whose service rate is μ=1/Treq(b)\mu = 1/T_{\text{req}}(b^\star) (requests/s). Give the formula for expected total response time WW (queue + service), and find the maximum λ\lambda per replica such that W500W \le 500 ms. State the utilisation ρ\rho at that point. (7)

(c) You must serve a sustained load of λtotal=120\lambda_{\text{total}} = 120 req/s. Using the per-replica capacity from (b), how many replicas NN are required? If each GPU replica costs $2.60/hour, compute the hourly serving cost and the cost per million requests at full load. (5)

(d) Post-quantization the model uses INT8 weights. Explain quantitatively why quantization can reduce both memory footprint and latency, and derive the theoretical memory saving factor when moving from FP16 to INT8 weights for a model with PP parameters. Then discuss one accuracy-preservation technique and one monitoring signal you would track to detect quantization-induced degradation in production. (7)


Question 2 — Data Drift Detection & Retraining Trigger (20 marks)

A production classifier scores incoming feature xx. During training, the feature followed PrefN(0,1)P_{\text{ref}} \sim \mathcal{N}(0, 1). In production over a monitoring window you observe QN(μ1,σ12)Q \sim \mathcal{N}(\mu_1, \sigma_1^2).

(a) The population stability index between two Gaussians via KL divergence is used as a drift score. Show that

DKL(QPref)=lnσ0σ1+σ12+(μ1μ0)22σ0212,D_{\text{KL}}(Q \| P_{\text{ref}}) = \ln\frac{\sigma_0}{\sigma_1} + \frac{\sigma_1^2 + (\mu_1-\mu_0)^2}{2\sigma_0^2} - \frac{1}{2},

with μ0=0, σ0=1\mu_0=0,\ \sigma_0=1. Then compute DKLD_{\text{KL}} numerically for the observed QN(0.5, 1.52)Q\sim\mathcal{N}(0.5,\ 1.5^2). (8)

(b) Distinguish data drift from concept drift with a precise probabilistic definition of each (P(x)P(x) vs P(yx)P(y\mid x)). Give a concrete example where accuracy stays high despite large data drift, and one where accuracy collapses with no input drift. (6)

(c) You configure an automated retraining pipeline. Define a statistically principled trigger: given a per-window drift statistic, explain how you would set the threshold to control false-positive retraining rate, and describe the CI/CD steps that must run automatically between "drift detected" and "new model serving live traffic." Include how versioning (data + model + registry) guarantees reproducibility of a rollback. (6)


Question 3 — Deployment Architecture & A/B Rollout (16 marks)

(a) Contrast batch vs real-time inference across: latency SLO, cost model, and freshness of features. State one workload best suited to each. (4)

(b) You roll out model B against incumbent A using a 50/50 A/B test. Baseline conversion (A) = 4.0%. You want to detect a lift to 4.4% (absolute) with power 0.8 at α=0.05\alpha=0.05 (two-sided). Using the two-proportion sample-size approximation

n(zα/2+zβ)2(pA(1pA)+pB(1pB))(pBpA)2,n \approx \frac{(z_{\alpha/2}+z_\beta)^2\,\big(p_A(1-p_A)+p_B(1-p_B)\big)}{(p_B-p_A)^2},

with zα/2=1.96, zβ=0.84z_{\alpha/2}=1.96,\ z_\beta=0.84, compute the required samples per arm. (6)

(c) Write a minimal FastAPI endpoint that (i) loads a model at startup, (ii) routes 10% of traffic to a canary model by a hash of the request id, and (iii) returns a prediction with the serving model version in the response. Explain why hashing the request id (not random routing) is important for consistency. (6)


Answer keyMark scheme & solutions

Question 1

(a) [5 marks] r(b)=Rmax/b=8000/br(b) = R_{\max}/b = 8000/b tokens/s per request.

Treq(b)=0.040+2568000/b=0.040+256b8000=0.040+0.032b s.T_{\text{req}}(b) = 0.040 + \frac{256}{8000/b} = 0.040 + \frac{256\,b}{8000} = 0.040 + 0.032\,b \text{ s}. (2 marks derivation)

Set 0.500\le 0.500: 0.032b0.460b14.3750.032\,b \le 0.460 \Rightarrow b \le 14.375. (2) Largest integer batch: b=14b^\star = 14. (1) Then Treq(14)=0.040+0.448=0.488T_{\text{req}}(14) = 0.040 + 0.448 = 0.488 s.

(b) [7 marks] Service rate μ=1/Treq(b)=1/0.488=2.049\mu = 1/T_{\text{req}}(b^\star) = 1/0.488 = 2.049 req/s. (1)

For M/M/1, expected response time (Little/queueing result): W=1μλ.(2)W = \frac{1}{\mu - \lambda}. \quad (2)

Require W0.5W \le 0.5: 1μλ0.5μλ2λμ2=2.0492=0.049\frac{1}{\mu-\lambda}\le 0.5 \Rightarrow \mu-\lambda \ge 2 \Rightarrow \lambda \le \mu - 2 = 2.049 - 2 = 0.049 req/s. (3)

Utilisation ρ=λ/μ=0.049/2.049=0.024\rho = \lambda/\mu = 0.049/2.049 = 0.024. (1)

Interpretation credit: Since service alone (0.488 s) nearly consumes the 500 ms budget, almost no queueing headroom remains — hence the tiny per-replica λ\lambda. This is the correct, if uncomfortable, conclusion. (Grader: full marks for correct method even noting the tightness.)

(c) [5 marks] Per-replica capacity λrep=0.049\lambda_{\text{rep}} = 0.049 req/s. N=λtotal/λrep=120/0.049=2449=2449 replicas.(2)N = \lceil \lambda_{\text{total}}/\lambda_{\text{rep}} \rceil = \lceil 120/0.049 \rceil = \lceil 2449 \rceil = 2449 \text{ replicas}. \quad (2) Hourly cost = 2449 \times \2.60 = $6367.4/\text{h}.(1)Requests/houratfullload. *(1)* Requests/hour at full load = 120 \times 3600 = 432{,}000.Costpermillionrequests. Cost per million requests = \dfrac{6367.4}{0.432} = $14{,}739$ per million. (2)

(Discussion note: such a strict p99≈service-time budget makes real-time serving expensive; relaxing the SLO or batching harder trades latency for cost. Recognising this earns full marks.)

(d) [7 marks]

  • Memory: FP16 = 2 bytes/param, INT8 = 1 byte/param. Footprint = P×P\times bytes. Saving factor =2 bytes/1 byte=2×= 2\text{ bytes}/1\text{ byte} = 2\times (weights halve). (2)
  • Latency: LLM decode is memory-bandwidth bound; time to stream weights \propto bytes moved. Halving weight bytes roughly halves the per-token weight-load time, and INT8 tensor-core throughput is higher than FP16, so latency drops (bandwidth + compute). (2)
  • Accuracy preservation: e.g. per-channel / group-wise quantization, calibration on representative data, or keeping outlier channels in higher precision (SmoothQuant / AWQ). Naming any valid technique with reason. (1.5)
  • Monitoring signal: track output quality / perplexity drift or downstream task metric, plus token-level distribution shift vs FP16 baseline, or user thumbs-down rate. (1.5)

Question 2

(a) [8 marks] KL between two Gaussians QN(μ1,σ12)Q\sim\mathcal N(\mu_1,\sigma_1^2), PN(μ0,σ02)P\sim\mathcal N(\mu_0,\sigma_0^2): DKL(QP)=EQ[lnq(x)p(x)].D_{\text{KL}}(Q\|P)=\mathbb E_Q\Big[\ln\frac{q(x)}{p(x)}\Big]. lnqp=lnσ0σ1(xμ1)22σ12+(xμ0)22σ02.\ln\frac{q}{p}=\ln\frac{\sigma_0}{\sigma_1} -\frac{(x-\mu_1)^2}{2\sigma_1^2}+\frac{(x-\mu_0)^2}{2\sigma_0^2}. Take EQ\mathbb E_Q: EQ[(xμ1)2]=σ12\mathbb E_Q[(x-\mu_1)^2]=\sigma_1^2, and EQ[(xμ0)2]=σ12+(μ1μ0)2\mathbb E_Q[(x-\mu_0)^2]=\sigma_1^2+(\mu_1-\mu_0)^2. (4) DKL=lnσ0σ1σ122σ12+σ12+(μ1μ0)22σ02=lnσ0σ1+σ12+(μ1μ0)22σ0212. (2)D_{\text{KL}}=\ln\frac{\sigma_0}{\sigma_1}-\frac{\sigma_1^2}{2\sigma_1^2}+\frac{\sigma_1^2+(\mu_1-\mu_0)^2}{2\sigma_0^2}=\ln\frac{\sigma_0}{\sigma_1}+\frac{\sigma_1^2+(\mu_1-\mu_0)^2}{2\sigma_0^2}-\frac12.\ \checkmark \quad (2)

Numeric with μ0=0,σ0=1,μ1=0.5,σ1=1.5\mu_0=0,\sigma_0=1,\mu_1=0.5,\sigma_1=1.5: ln(1/1.5)=0.405465,2.25+0.252=1.25,\ln(1/1.5)=-0.405465,\quad \frac{2.25+0.25}{2}=1.25, DKL=0.405465+1.250.5=0.344535 nats.(2)D_{\text{KL}}=-0.405465+1.25-0.5=0.344535\ \text{nats}. \quad (2)

(b) [6 marks]

  • Data drift (covariate shift): input distribution changes, Pprod(x)Ptrain(x)P_{\text{prod}}(x)\ne P_{\text{train}}(x), while P(yx)P(y\mid x) unchanged. (1.5)
  • Concept drift: the mapping changes, Pprod(yx)Ptrain(yx)P_{\text{prod}}(y\mid x)\ne P_{\text{train}}(y\mid x). (1.5)
  • High accuracy despite drift: input distribution shifts into a region where the decision boundary is still correct (e.g. more traffic from a demographic the model already handles well). (1.5)
  • Accuracy collapse with no input drift: same P(x)P(x) but labels' relationship changed — e.g. fraud patterns evolve, spam wording stays but "spam vs not" definition flips. (1.5)

(c) [6 marks]

  • Threshold via null distribution: estimate the drift statistic's distribution under "no drift" (bootstrap over historical stable windows). Set threshold at a quantile controlling per-window false-positive rate α\alpha; apply multiple-comparison / windowing correction to bound the expected number of false retrains. (2)
  • Automated CI/CD chain: drift alert → trigger retraining job on versioned data snapshot → automated eval on held-out + shadow traffic → gate on metric threshold → register new model in registry with lineage → canary deploy → promote. (2)
  • Versioning for rollback: DVC pins the exact dataset hash, MLflow/registry pins model artifact + code commit + hyperparams; a rollback = redeploy the previous registry version, guaranteed reproducible because all three (data, code, model) are content-addressed. (2)

Question 3

(a) [4 marks]

Batch Real-time
Latency SLO minutes–hours (throughput-oriented) ms (per-request)
Cost cheap, spot/idle GPUs, amortised expensive, always-on replicas
Feature freshness stale (as of last batch) fresh/online features

Example: batch → nightly recommendation precompute; real-time → fraud scoring at checkout. (4: 1 per row + example)

(b) [6 marks] pA=0.04, pB=0.044, (zα/2+zβ)2=(1.96+0.84)2=7.84p_A=0.04,\ p_B=0.044,\ (z_{\alpha/2}+z_\beta)^2=(1.96+0.84)^2=7.84. (1) pA(1pA)=0.0384,pB(1pB)=0.0440.956=0.042064p_A(1-p_A)=0.0384,\quad p_B(1-p_B)=0.044\cdot0.956=0.042064. Sum =0.080464=0.080464. (2) (pBpA)2=(0.004)2=1.6×105(p_B-p_A)^2=(0.004)^2=1.6\times10^{-5}. (1) n=7.84×0.0804641.6×105=0.6308381.6×10539,427 per arm.(2)n=\frac{7.84\times0.080464}{1.6\times10^{-5}}=\frac{0.630838}{1.6\times10^{-5}}\approx 39{,}427 \text{ per arm}. \quad (2)

(c) [6 marks]

import hashlib
from fastapi import FastAPI, Request
import joblib
 
app = FastAPI()
models = {}
 
@app.on_event("startup")
def load():
    models["A"] = joblib.load("model_a.pkl")   # stable
    models["B"] = joblib.load("model_b.pkl")   # canary
 
def route(req_id: str) -> str:
    h = int(hashlib.md5(req_id.encode()).hexdigest(), 16)
    return "B" if (h % 100) < 10 else "A"      # 10% canary
 
@app.post("/predict")
async def predict(request: Request):
    body = await request.json()
    req_id = body["request_id"]
    version = route(req_id)
    pred = models[version].predict([body["features"]])[0]
    return {"prediction": float(pred), "model_version": version}

(4 marks: startup load, hash routing 10%, version in response)

Why hash the request id: deterministic routing means the same user/request always hits the same model — consistent user experience, valid A/B attribution, and no flip-flop between refreshes. Pure random routing would send repeat requests to different models, contaminating measurement and confusing users. (2)

[
  {"claim":"Q1a largest batch size b*=14 gives Treq=0.488s <=0.5","code":"b=14; T=0.040+0.032*b; result=(T<=0.5) and (0.040+0.032*15>0.5)"},
  {"claim":"Q1b lambda max = 1/0.488 - 2 approx 0.0492","code":"mu=1/0.488; lam=mu-2; result=abs(lam-0.04918)<1e-3"},
  {"claim":"Q1c N=2449 replicas for 120 req/s","code":"import math; lam=1/0.488-2; N=math.ceil(120/lam); result=(N==2449)"},
  {"claim":"Q2a KL(N(0.5,1.5^2)||N(0,1)) approx 0.34453","code":"kl=ln(Rational(1,1)/Rational(3,2))+(Rational(9,4)+Rational(1,4))/2-Rational(1,2); result=abs(float(kl)-0.344535)<1e-4"},
  {"claim":"Q3b per-arm n approx 39427","code":"n=(7.84*(0.04*0.96+0.044*0.956))/(0.004**2); result=abs(n-39427)<50"}
]