Drill 89 — Which Model Decided?
Theme: Model Lifecycle & MLOps · Format: 20-min drill · Difficulty: core
The story
A digital lender approves and declines credit-card applications with a machine-learning model. It has grown up the way most production models do: a data scientist trains a candidate in a notebook, exports it, and the platform team copies the file to s3://models/credit/current.pkl. The scoring service loads whatever sits at that path. When someone ships a better model, they overwrite current.pkl. Deploys are a file copy; rollbacks are copying the old file back — if anyone still has it.
For eight months this is invisibly fine. Applications come in, scores come out, adverse-action letters go to the declined. Nothing errors. Then a declined applicant files a complaint, and under the Equal Credit Opportunity Act the regulator sends a routine request: for this specific application, on this date, tell us which model scored it, what inputs it saw, and why it produced a decline.
The team pulls the thread and finds nothing to hold. The scoring logs record the application ID, the timestamp, and the final decision — but not which model produced it. There is no model version, because the artifact was never versioned; it was current.pkl, and current.pkl has been overwritten four times since that date. The exact feature vector the model saw wasn't logged either — the service recomputes features from a warehouse that has itself been backfilled twice. And the notebook that trained the model in question ran against a data snapshot no one pinned, on a library version no one recorded, with no fixed seed. Retraining it today produces a different model. Nobody did anything reckless; every step looked like normal shipping.
⏸ Stop here and ask the room: the model works, the decision was made, the letter went out. So why can nobody in this building say which model made it, or reproduce the decision the regulator is asking about?
Run it — facilitator script
Timing for a 20-minute session: 2 min story · 10 min staged questions · 3 min reveal · 5 min transfer.
The regulator wants the exact model that decided this application. What do you need to have logged — at decision time — to answer that?
- Listen for: a decision record written synchronously with every decision that captures the model version (a resolvable identifier, not "current"), the exact feature vector the model actually scored (or a hash + a snapshot you can rehydrate), the input/application hash, and the timestamp. Lineage is something you write down when the decision happens — it cannot be reconstructed later from a mutable warehouse.
- Weak signal: "we log the decision and the score." A score with no version and no inputs is unfalsifiable — it tells you what was decided, never by which model on what data, which is the entire question.
The artifact was current.pkl, overwritten four times. Where did versioning need to live, and why isn't a filename enough?
- Listen for: a model registry holding every promoted model as an immutable, content-addressed version (
credit-scorer:v37, pinned to an artifact hash), with the scoring service loading a specific version and logging that version on every call. Mutating a path in place destroys history; a filename is not an identity. Promotion should register a new version, never overwrite. - Weak signal: "we could keep old copies in a folder" or "add a date to the filename." Naming conventions maintained by hand are not a guarantee; the registry has to be the only way a model reaches production.
Even with the version, retraining it produces a different model. What makes a training run reproducible?
- Listen for: pinned everything — an immutable dataset snapshot ID, the exact code (git SHA), pinned library/framework versions, and a fixed random seed — all recorded as the version's lineage. Reproducibility means re-running the pinned inputs yields the same artifact (or the same metrics within a tiny tolerance), which is what lets you defend or explain a decision months later.
- Weak signal: "the notebook is in the repo." A notebook against a moving warehouse with no seed is a recipe, not a record — it can't reproduce the specific model that made the specific decision.
The reveal
There was no way to answer the regulator, because the answer was never written down. The fix wasn't a smarter model — it was a lifecycle. They stood up a model registry: every promoted model becomes an immutable version pinned to an artifact hash, and the scoring service can only load a registered version, never a bare file. They added a decision log: one row per decision, written synchronously, carrying model_version, a snapshot of the exact feature vector, an input hash, and the timestamp. They made training reproducible — pinned dataset snapshot, git SHA, library versions, fixed seed, all stored as the version's lineage. And they set a retention policy matching the ECOA/Reg B audit window so none of it could age out before a regulator could ask. The next request took minutes: version, inputs, and the reproduced decision, on demand.
Why this happens — the mechanics
A machine-learning decision is a function of three things that all move independently: the model artifact, the exact inputs it scored, and the code/data that produced the artifact. Traditional software has one of these — the code — and version control already pins it. ML adds two more moving parts, and teams that carry over "just deploy the file" habits from app code silently drop the other two. The decision is fully determined at inference time and then the evidence evaporates: the artifact gets overwritten, the feature warehouse gets backfilled, the training data drifts. Nothing errors, because nothing is supposed to be preserved — no line of code asked for it.
The organizational root is that lineage is a cross-cutting concern nobody owns until an auditor creates the owner. The data scientist's job ends at "good metrics," the platform team's job is "the file is served," and no single seam is responsible for "we can reconstruct any past decision." In regulated lending this isn't optional hygiene — ECOA requires a specific reason for every adverse action, and model-risk guidance (SR 11-7) expects you to know exactly which model, trained how, made which call. The failure is invisible right up until someone outside the building asks a question that the inside of the building never wrote down the answer to.
Diagrams
Problem — how the decision becomes unreconstructable:
sequenceDiagram
autonumber
participant AP as Applicant
participant SVC as Scoring service
participant ART as Model file (S3)
participant REG as Regulator
participant ML as ML / platform team
AP->>SVC: Apply for credit
Note over SVC: Score with current.pkl<br/>no version recorded
SVC-->>AP: Declined (adverse action)
Note over ART: current.pkl overwritten<br/>4 times since (silent)
REG->>ML: Which model declined this?
ML--xART: Fetch the exact model<br/>that scored it
Note over ML,REG: No version, no inputs,<br/>artifact gone — no answer
Solution — where the lineage guardrails go:
flowchart TD
A["Application scored"] --> B["Decision log row<br/>(version + feature<br/>snapshot + input hash)"]
T["Training run"] --> P["Pinned data + code<br/>+ seed (reproducible)"]
P --> R["Model registry<br/>(immutable versions)"]
B --> R
R -.-> AL["Audit query resolves<br/>a past decision"]
B -.-> AL
RT["Retention policy<br/>(matches audit window)"] -.-> AL
AL --> H["Reconstruct exact<br/>decision on demand"]
What good looks like
- A model registry as the only path to production. Every promoted model is an immutable, content-addressed version (
credit-scorer:v37pinned to an artifact hash); the scoring service loads a specific version and can never load a bare file. Block any deploy whose artifact hash isn't registered; target 100% of production scores carrying a resolvablemodel_version. - A synchronous decision log, one row per decision. Write
model_version, the exact feature vector scored (or a hash plus a rehydratable snapshot), an input/application hash, and the timestamp — at decision time, in the same transaction as the decision. Alert if any adverse-action decision lands without a matching lineage row; that count must be 0. - Reproducible training, pinned end to end. Record the dataset snapshot ID, git SHA, library/framework versions, and a fixed seed as the version's lineage. A re-run from the pinned inputs must reproduce the artifact's headline metric (e.g. AUC) within a tight tolerance (say ≤ 0.001); CI fails any training run that produces a model with no recorded lineage.
- A retention policy that matches the audit window. Keep model artifacts and decision logs for at least the regulator's window (for ECOA adverse-action, retain the record 25 months; longer where model-risk policy demands). Make deletion refuse to remove any artifact still referenced by an in-window decision.
- An audit-reconstruction game day. Quarterly, pick a random past decision and reconstruct it end to end — version, inputs, and reproduced score — with a stopwatch. If it takes more than an hour, or you can't, the lineage is already broken; find out now, not under subpoena.
The lesson
An ML decision is only as auditable as what you wrote down at the moment it was made — the model version, the exact inputs, and the recipe that built the model. Version artifacts immutably, log every decision's lineage synchronously, and keep it as long as someone can ask; you cannot reconstruct after the fact what you never recorded.
Named concepts: model registry & versioning, decision logging & lineage, reproducible training, audit retention policy.
Real-world echoes
- The Apple Card / Goldman Sachs credit-limit investigation (NY DFS, 2019). After public complaints that the card offered different credit limits in ways applicants couldn't understand, the New York Department of Financial Services opened an investigation, and the recurring difficulty was explaining individual algorithmic credit decisions after the fact. The same-class lesson: if you can't tie each decision to the exact model and inputs that produced it, "the model decided" is not an answer a regulator will accept.
Transfer
For your highest-stakes model, pick one real decision it made last quarter and try to answer the regulator's three questions: which version, on what inputs, trained how? If you can't produce all three in an hour, your decisions aren't auditable — you just haven't been asked yet.
Make it a ticket: stand up a model registry so nothing reaches production except a registered immutable version, add a synchronous decision log capturing model_version + feature snapshot + input hash on every scored application, pin training (dataset snapshot + git SHA + seed) into each version's lineage, and set retention to the audit window — with a game day that reconstructs a random past decision end to end.
Related
- Drill 87 — Train-Serve Skew — the sibling in this theme: skew is about which features the model saw; this is about proving which model saw them and being able to reconstruct the whole decision.
- Drill 1 — The Model That Scored Everyone the Same — the same silent-ML blindness: a system that returns confident outputs while the thing you actually need to know goes unrecorded.
- Drill 88 — The Drift Nobody Watched, theme 15 — lineage is what lets you go back and prove when a model changed and which version was live at a given time.
- Pattern #138 — Schema / Data Contracts — the reusable enforcement that keeps a logged feature snapshot meaningful over time.
